<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<?xml-stylesheet href="/feed.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>Nathan Manceaux-Panot</name>
    <uri>https://cykele.ro/</uri>
  </author>
  <id>urn:uuid:64900eb8-2ce5-477c-a4c6-0f9dcd541876</id>
  
  <link rel="self" type="application/atom+xml" href="https://pending.design/feed.atom.xml" hreflang="en-us" />
  
  
  <link rel="alternate" type="text/html" href="https://pending.design/" hreflang="en-us" />
  
  <icon>https://pending.design/images/icon-512.png</icon>
  
  
  <title>Pending Design</title>
  <updated>2026-05-28T13:58:32+00:00</updated>
  
  <entry>
    <author>
      <name>Nathan Manceaux-Panot</name>
      <uri>https://cykele.ro/</uri>
    </author>
    <id>tag:pending.design,2026:/buttons-not-for-clicking/</id>
    <link rel="alternate" href="https://pending.design/buttons-not-for-clicking/" />
    <title>Buttons not for clicking</title>
    <published>2026-05-28T15:56:38+02:00</published>
    <updated>2026-05-28T15:56:38+02:00</updated>
    <summary type="text">There are buttons with unintuitive purpose in Nova’s Quick Open panel.</summary>
    <content type="html" xml:base="https://pending.design/" xml:lang="en">
      <![CDATA[<p>Writing about how some of Retcon’s menus <a href="/mom-only-git-client/">are purely informative</a> a few days ago made me notice a similar example in <a href="https://nova.app">Nova</a>, Panic’s code editor for macOS.</p>
<p>Like many power-user apps these days, Nova has an “open quickly” palette:</p>


<figure class="no-frame"><picture>
    <source srcset="screenshot-open-quickly-neutral-dark.png"
      alt="Screenshot of a small, wide floating window. At the top, an icon of a folder with a magnifying glass, and an empty text field, with an “Open Quickly” prompt. Below the field, four buttons: “All”, “Files /”, “Symbols #”, and “Symbols in Open Tabs @”. Below that still, a label: “Type a filename or symbol to quickly open it.”." width="559" height="204"media="(prefers-color-scheme: dark)"
    >
    <img src="screenshot-open-quickly-neutral-light.png"
      alt="Screenshot of a small, wide floating window. At the top, an icon of a folder with a magnifying glass, and an empty text field, with an “Open Quickly” prompt. Below the field, four buttons: “All”, “Files /”, “Symbols #”, and “Symbols in Open Tabs @”. Below that still, a label: “Type a filename or symbol to quickly open it.”." width="559" height="204">
  </picture>
</figure>

<p>Type a few letters, and you’re able to jump to any file, or symbol (an important element within a file):</p>


<figure class="no-frame"><picture>
    <source srcset="screenshot-open-quickly-search-all-dark.png"
      alt="The same window as before. In the text field, the word “release” has been typed. As a result, a list has appeared below the row of buttons: it lists various elements named “release”, a few CSS selectors like “.release &gt; ul”, and a file named “releases.html”. Each element has an icon indicating its type, and the relative path of the file it is from, such as “press/Retcon Press Kit/Retcon releases.template.html”." width="559" height="482"media="(prefers-color-scheme: dark)"
    >
    <img src="screenshot-open-quickly-search-all-light.png"
      alt="The same window as before. In the text field, the word “release” has been typed. As a result, a list has appeared below the row of buttons: it lists various elements named “release”, a few CSS selectors like “.release &gt; ul”, and a file named “releases.html”. Each element has an icon indicating its type, and the relative path of the file it is from, such as “press/Retcon Press Kit/Retcon releases.template.html”." width="559" height="482">
  </picture>
</figure>

<p>Through buttons right below its text field, the bar also lets you filter results: only show files, only show symbols, or only show symbols in current tabs. Here’s the thing, though: each one of these buttons has <em>four distinct purposes</em>. They’re not just for clicking.</p>
<p><strong>First,</strong> they indicate that you can filter at all! If filtering was done through an app menu instead, it’d be a lot harder to discover. You wouldn’t go looking for a feature you don’t even know exists. So before you’ve even clicked one of the buttons, they’re already doing a useful job; their presence itself is documentation.</p>
<p><strong>Second</strong> and most obvious, the buttons let you act. Click one and its filter is enabled:</p>


<figure class="no-frame"><picture>
    <source srcset="screenshot-open-quickly-filter-symbols-dark.png"
      alt="The same window again, with no results, and a single “#” character in the text field. The “Symbols” button is toggled on." width="559" height="204"media="(prefers-color-scheme: dark)"
    >
    <img src="screenshot-open-quickly-filter-symbols-light.png"
      alt="The same window again, with no results, and a single “#” character in the text field. The “Symbols” button is toggled on." width="559" height="204">
  </picture><figcaption>
      <p>Clicking the Symbols filter when the field is empty.</p>
    </figcaption>
</figure>

<p>Straightforward, except for an unassuming side-effect: a <code>#</code> was inserted into the text field. That’s right: <strong>the third purpose</strong> of the button is to teach you how to <em>stop</em> using it, how to graduate to a faster interaction.</p>
<p>You can enable a filter just by typing the corresponding magic prefix, which is especially handy since your hands will be on the keyboard anyway. But once again, you first need to learn that this is possible at all. By inserting the prefix whenever you enable a filter, the buttons tell you that the two are inextricably linked; they suggest that you could have typed the prefix yourself. No tutorial text, no need to go read a manual; just by using the app, you’re being taught not only what it’s capable of, but how to become faster at using it.</p>
<p>Finally, the button’s <strong>fourth and final purpose</strong> is what comes after all that. You’ve been taught about filters; taught about the magic prefixes; the button’s only job from now on will be acting as a cheat sheet. Each button displays the correct magic prefix in its label, acting as a reminder of what it is you must type, conveniently displayed exactly when you need it (and only then). Once you’ve learned all of the app’s tricks, the buttons won’t ever see another click, and yet they remain useful; they supplement your memory, lowering the cognitive load of going through the interface.</p>


]]>
    </content>
  </entry>
  
  <entry>
    <author>
      <name>Nathan Manceaux-Panot</name>
      <uri>https://cykele.ro/</uri>
    </author>
    <id>tag:pending.design,2026:/mom-only-git-client/</id>
    <link rel="alternate" href="https://pending.design/mom-only-git-client/" />
    <title>The Git client only my mom understands</title>
    <published>2026-05-26T14:37:21+02:00</published>
    <updated>2026-05-26T14:37:21+02:00</updated>
    <summary type="text">Beginner-friendly language empowers some, confuses others.</summary>
    <content type="html" xml:base="https://pending.design/" xml:lang="en">
      <![CDATA[
<p>Satisfied with the first implementation of cherry picking in Retcon, I sent a beta build to a couple of developer friends. Their instructions were to try out cherry picking, but they weren’t told <em>how</em> to use it: I wanted to make sure the feature was easy to find. Not long after receiving the beta, both my friends replied in the same way: <em>Nathan, you sent me the wrong build; this one doesn’t have cherry picking.</em> But it did!</p>
<p>I sat my mom — a tech-savvy Mac user, but one who’d never used Git in her life — in front of the same beta, and tasked her with bringing some specific commit to some specific branch. Just minutes later, with little effort, she had done so.</p>
<p>A feature that baffles experienced users, and comes naturally to novices. How did I mess up that bad?</p>
<h2 id="embracing-the-gui">Embracing the GUI</h2>
<aside>
	(For context, <strong>Retcon</strong> is <a href="https://retcon.app/">a new kind of Git client for macOS</a>. It lets you manipulate your commit history with ease, replacing the usual complexities and constraints with clarity and flexibility. <strong>Cherry picking</strong> is an important feature in Retcon, and Git clients in general, which lets you copy a commit from one branch to another.)
</aside>

<p>So, what did that misguided cherry picking interface look like?</p>
<p>There’s an obvious answer for what it <em>could</em> have looked like. A small modal window, in which you can type a commit hash or name, or maybe choose from a list. Click OK, and the commit is inserted.</p>


<figure class="no-frame"><picture>
    <source srcset="sketch-conventional-solution-dark.png"
      alt="A sketch of a Mac window. The title says “Cherry Pick”, and a subtitle reads “Choose commit to insert in main.” Below that, a dropdown menu: the user has typed “Update palette”, relying on autocomplete to insert the last few letters. The commit’s hash is also displayed in the dropdown. Finally, at the bottom of the window, there are a Cancel button and a Rebase button." width="512" height="341"media="(prefers-color-scheme: dark)"
    >
    <img src="sketch-conventional-solution-light.png"
      alt="A sketch of a Mac window. The title says “Cherry Pick”, and a subtitle reads “Choose commit to insert in main.” Below that, a dropdown menu: the user has typed “Update palette”, relying on autocomplete to insert the last few letters. The commit’s hash is also displayed in the dropdown. Finally, at the bottom of the window, there are a Cancel button and a Rebase button." width="512" height="341">
  </picture><figcaption>
      <p>“I don’t want to do design”-type design.</p>
    </figcaption>
</figure>

<p>With Retcon however, I try to never settle for the obvious and conventional, in the hope of finding new, better solutions. The app’s whole schtick is that of improvement through reinvention.</p>
<p>(Sometimes, the obvious and conventional is perfectly good, and therefore makes it into Retcon, of course.)</p>
<p>In the case of cherry picking, it turns out there’s an existing metaphor in the vocabulary of <abbr>GUI</abbr> interactions; a perfect fit, that both feels natural to use and is widely understood: copy and paste! Copy something from somewhere, paste a copy of it elsewhere: that’s exactly what cherry picking is. So that would be the model: just copy and paste commits.</p>
<p>The elegance of that solution, however, is also its weak point. It requires absolutely no new buttons or menus or anything — the Copy and Paste commands are already available in the Edit menu — making the feature invisible. How will users discover that they <em>can</em> copy-and-paste commits? There’s no precedent of a Git client letting you do that.</p>
<p>Surfacing commit copy-and-paste would require adding <em>something</em>. I decided on a somewhat awkward solution:</p>
<figure><img src="/mom-only-git-client/screenshot-initial-solution.png"
    alt="A screenshot of a macOS menu. It’s the Commit menu. Among its various items, there’s a group of three: Cut, Copy, and Paste. Their shortcuts are the usual ⌘X, ⌘C and ⌘V." width="634" height="463"><figcaption>
      <p>The standard Cut/Copy/Paste items, duplicated in the Commit menu.</p>
    </figcaption>
</figure>

<p>Placing a copy of the clipboard commands in the Commit menu, while unusual, sends a clear message: these operations apply to commits. With this, Retcon’s first cherry pick interface was complete. As you read at the start, though, it did not fare well with experienced users. Why not?</p>
<h2 id="trapped-in-domain-knowledge">Trapped in domain knowledge</h2>
<p>My developer friends, tasked with cherry picking, scanned the menus for the words “cherry pick”. It’s that simple: they knew the terminology, and so that’s what they looked for<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>, and did not find. Interestingly, one of them did find the copy/paste commands once I gave them a clue (“have another look in the Commit menu!”), while the other, with the same indication, <em>still found nothing</em>. That’s how strongly they associated the domain-specific term with the action.</p>
<p>On the other hand, my mom, equipped with no Git-specific knowledge, was free to think more flexibly. Need to copy something from A to B, while not being able to see source and destination at the same time? That’s what the clipboard is for. There, done. The answer came perfectly naturally.</p>
<p>By choosing a less specialized, more widely understandable metaphor for the action, I both made the feature more accessible to people without any domain knowledge, and less intuitive to those <em>with</em> domain knowledge. What I expected to be a pure improvement to usability, wasn’t<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>.</p>
<aside>
	(Why does the term “cherry pick” exist in the first place? It’s needed in the context of the command line, where copy-paste isn’t a thing: a specific action had to be made up just for the act of copying a commit over. That same term becomes much less useful in a <abbr>GUI</abbr>: the rich set of shared concepts includes an appropriate metaphor already.)
</aside>

<h2 id="dumbing-it-up">Dumbing it up</h2>
<p>That initial approach had turned out to be too subtle, and needed fixing. I very much wanted to keep the flexibility and approachability of the copy-paste model, but needed to also make its presence clear to experienced users. My first attempt was tragically inelegant, if effective:</p>
<figure><img src="/mom-only-git-client/screenshot-suffix-solution.png"
    alt="A screenshot similar to the previous one, except this time, the Paste command has a suffix. It says “Cherry Pick”, in parentheses, and is moderately grayed out." width="634" height="463"><figcaption>
      <p>The Paste item now has a suffix.</p>
    </figcaption>
</figure>

<p>This works — anyone looking for the term of art will indeed find it — but the Paste command now looks way, <em>way</em> too conspicuous. It’s extremely rare for menu items to have a suffix that looks like that, which raises questions. Is this menu special? Does it have some sort of secondary action that can be invoked somehow? Or is cherry picking currently disabled for some reason? That solution felt like it would indeed achieve its goal of calling attention to the command, but that it’d cause confusion in the process.</p>
<p>Having run out of ideas, I had resigned to ship this problematic-but-better-than-nothing implementation, until finally, it struck. A way to label a group of menus? That’s a submenu!</p>
<figure><img src="/mom-only-git-client/screenshot-submenu-solution.png"
    alt="A screenshot similar to the previous one. The Paste command no longer has a suffix. The three Cut, Copy and Paste commands are however now nested in a submenu named “Cherry Pick”." width="634" height="463"><figcaption>
      <p>The Cut/Copy/Paste commands, lovingly nestled in a submenu.</p>
    </figcaption>
</figure>

<p>A submenu! Of course! I sent excited, celebratory messages to many friends at that point.</p>
<p>The submenu approach has it all. There’s no attention-grabbing suffix; on the contrary, the three options are tucked away, which appropriately de-emphasizes them (I don’t expect people to ever <em>use</em> the menus to cherry pick; they’re just here to teach about the feature<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>). The nesting reinforces the message that these actions work together. And, most importantly, the submenu’s name does a perfect job of both explaining what the options are for, and of guiding those who look for that specific term. Hurray!</p>
<p>This submenu is what <a href="https://retcon.app/releases#retcon-1.6">shipped in Retcon 1.6</a>, and so far, no one’s asked where the cherry picking button was.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Admittedly, by telling my friends to “test cherry picking in this beta build”, I made them think of the term; maybe they’d have succeeded at the task if I’d given them higher-level instructions, like I did with my mom. Arguably though, all I did was unwittingly set up a worst-case scenario that would have occurred in the wild anyway.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>This isn’t the first time I run into this pro/novice tension. After all, I’m always looking to improve on Git’s existing metaphors, workflows, and terminology; that’s the whole reason Retcon exists! Lots of <abbr>GUI</abbr>s offer history rewriting already, but none comes close to Retcon’s speed and ease of use, because they’re all shackled to the Git command line’s model.</p>
<p>One previous example was the word “fixup”. It’s completely opaque: you really can’t guess what it means, you have to experiment, or look it up. But no alternative I could find ever worked. “Flatten” is just as mysterious. “Combine”, “join” and “fuse” seem to imply more than what a fixup actually does. Most vexing of all, the one verb that perfectly describes the operation is already taken: “merge”. In the end, the term of art won, and it’ll keep confusing beginners, even those expecting better from Retcon.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>App menus have a fascinating dual role as both interactive buttons, and passive documentation. They’re a hugely under-appreciated part of the modern <abbr>GUI</abbr>. I hope to write about them in the future.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]>
    </content>
  </entry>
  
  <entry>
    <author>
      <name>Nathan Manceaux-Panot</name>
      <uri>https://cykele.ro/</uri>
    </author>
    <id>tag:pending.design,2026:/unreasonable-picker-behavior/</id>
    <link rel="alternate" href="https://pending.design/unreasonable-picker-behavior/" />
    <title>SwiftUI Picker: Unreasonable tricks for reasonable behavior</title>
    <published>2026-03-12T14:44:58+01:00</published>
    <updated>2026-03-12T14:44:58+01:00</updated>
    <summary type="text">Fixing performance and cosmetic issues in creative ways.</summary>
    <content type="html" xml:base="https://pending.design/" xml:lang="en">
      <![CDATA[
<p><a href="https://lemon.garden/dye/">Dye</a> is a very simple app, with a very simple UI. It lists installed applications, each with a pop-up menu for its theme color. That’s about it.</p>


<figure class="no-frame"><picture>
    <source srcset="screenshot-dye-dark.png"
      alt="Screenshot of macOS app Dye. The window is mostly just a list, each row representing an app—Finder, Mail, Music, etc—and offering a theme color picker. The various rows are set to explicit colors—yellow, green, etc—or sometimes to special values, Automatic or App default. The toolbar contains a search field in its toolbar, and the apps are split into two sections: Open Apps, and All Apps." width="657" height="784"media="(prefers-color-scheme: dark)"
    >
    <img src="screenshot-dye-light.png"
      alt="Screenshot of macOS app Dye. The window is mostly just a list, each row representing an app—Finder, Mail, Music, etc—and offering a theme color picker. The various rows are set to explicit colors—yellow, green, etc—or sometimes to special values, Automatic or App default. The toolbar contains a search field in its toolbar, and the apps are split into two sections: Open Apps, and All Apps." width="657" height="784">
  </picture><figcaption>
      <p>Dye: just a list.</p>
    </figcaption>
</figure>

<p>Despite this simplicity, the first implementation of that list was shockingly badly-behaved. It was:</p>
<ol>
<li>Extremely slow (scrolling would cause multi-second freezes!)</li>
<li>Visually broken in multiple ways (you’ll see)</li>
</ol>
<p>The implementation was naive but reasonable. The output, anything but. How come?</p>

<h2 id="using-images-in-picker-options">Using images in <code>Picker</code> options</h2>
<p>Look, I like colors. So if my app has a whole menu of them, I want to show cute little swatches:</p>


<figure><picture>
    <source srcset="screenshot-menu-good-dark.png"
      alt="A pop-up menu. It lists colors—from Graphite to Pink—preceded by two special entries, Automatic and App default. Each item prefixes its title with a circle of the represented color." width="196" height="325"media="(prefers-color-scheme: dark)"
    >
    <img src="screenshot-menu-good-light.png"
      alt="A pop-up menu. It lists colors—from Graphite to Pink—preceded by two special entries, Automatic and App default. Each item prefixes its title with a circle of the represented color." width="196" height="325">
  </picture>
</figure>

<p>That didn’t work at first, though. For its options, <code>Picker</code> only accepts some text, plus an image; anything else seems to get ignored. So this sensible-looking piece of code plain doesn’t work:</p>
<pre><code class="language-swift">struct AccentColorPicker: View {
	@Binding var selection: AccentColor
	
	var body: some View {
		Picker(&quot;Accent color&quot;, selection: $selection) {
			ForEach(AccentColor.allCases) { accentColor in
				HStack {
					Circle()
						.fill(accentColor.color)
						.strokeBorder(.tertiary)
					
					Text(accentColor.name)
				}
			}
		}
	}
}
</code></pre>


<figure><picture>
    <source srcset="screenshot-menu-no-swatches-dark.png"
      alt="The same menu as above, except there are no color circles at all." width="196" height="325"media="(prefers-color-scheme: dark)"
    >
    <img src="screenshot-menu-no-swatches-light.png"
      alt="The same menu as above, except there are no color circles at all." width="196" height="325">
  </picture><figcaption>
      <p>SwiftUI ignores the <code>Circle</code> views.</p>
    </figcaption>
</figure>

<p>Well, if an image is what it takes, we can make one. Dye’s solution is to make use of SwiftUI’s <a href="https://developer.apple.com/documentation/swiftui/imagerenderer"><code>ImageRenderer</code></a>, a class for rendering a view into an image. We create our own <code>Rasterize</code> view, which draws into content view into an image; then we wrap the circle with it. This works!</p>
<pre><code class="language-swift">// Defining Rasterize
struct Rasterize&lt;Content: View&gt;: View {
	@ViewBuilder var content: Content
	
	var body: some View {
		if let nsImage = ImageRenderer(content: content).nsImage {
			Image(nsImage: nsImage)
		}
	}
}

// Using it
struct AccentColorPicker: View {
	@Binding var selection: AccentColor
	
	var body: some View {
		Picker(&quot;Accent color&quot;, selection: $selection) {
			ForEach(AccentColor.allCases) { accentColor in
				HStack {
					Rasterize { // here!
						Circle()
							.fill(accentColor.color)
							.strokeBorder(.tertiary)
					}
					
					Text(accentColor.name)
				}
			}
		}
	}
}
</code></pre>


<figure><picture>
    <source srcset="screenshot-menu-bad-spacing-dark.png"
      alt="The same menu as before, except the options now have their color circles. However, in the closed menu’s button, the swatch-to-text spacing is different than that from the items’." width="196" height="325"media="(prefers-color-scheme: dark)"
    >
    <img src="screenshot-menu-bad-spacing-light.png"
      alt="The same menu as before, except the options now have their color circles. However, in the closed menu’s button, the swatch-to-text spacing is different than that from the items’." width="196" height="325">
  </picture>
</figure>

<p>The main drawback here is that the <code>ImageRenderer</code>’s view tree is disconnected from the main view tree; as a result, the environment must be bridged over explicitly (as seen in <a href="https://codeberg.org/Cykelero/Dye/src/commit/5d64860a261a3f310e0238a73b16199cf8744c4a/Dye/Support/Views/Rasterize.swift">the full source to <code>Rasterize</code></a>), which breaks dependency tracking, making the user responsible for keeping the environment up-to-date. It works, though!</p>
<h3 id="fixing-the-spacing-inconsistency">Fixing the spacing inconsistency</h3>
<p>Look at the screenshot above again, though. There’s something wrong with the space between swatches and text: it’s inconsistent between in the popup button at the very top, and the menu items below! Yikes.</p>
<p>I tried a few variations—using a <code>Label</code> instead of an <code>HStack</code>, for instance—but the output was always the same. It looks like that’s just how <code>Picker</code> looks on macOS.</p>
<p>So let’s roll up our sleeves, and reimplement the popup button’s appearance. <a href="https://codeberg.org/Cykelero/Dye/commit/302c169c026b0177c36b858afe01c1996374d0bd#diff-be3eed24a38e5007bac03e986f1028e2a02d6f33">It’s not that much code</a>, and the double arrow is conveniently available as a SF symbol. What you see in the app, then, is a totally fake button; but overlaid on top is the real <code>Picker</code> button, set to near-zero opacity.</p>
<p>That’s an absurd amount of work for a basic piece of UI, but it works!</p>


<figure><picture>
    <source srcset="screenshot-menu-good-dark.png"
      alt="The same menu still, but with color circles, all correctly spaced." width="196" height="325"media="(prefers-color-scheme: dark)"
    >
    <img src="screenshot-menu-good-light.png"
      alt="The same menu still, but with color circles, all correctly spaced." width="196" height="325">
  </picture>
</figure>

<aside>
	(In the final app, the pickers look different from this. To my eye, the platter’s default appearance clashed with the alternating row backgrounds, so I made them <a href="https://codeberg.org/Cykelero/Dye/src/commit/5d64860a261a3f310e0238a73b16199cf8744c4a/Dye/Support/Views/FakePicker.swift#L43">opaque</a>.)
</aside>

<h2 id="making-picker-fast-in-two-parts">Making Picker fast (in two parts)</h2>
<p>Okay, the <code>Picker</code> looks good, now. But, surprise surprise, scrolling the list is extremely choppy. Like stop motion, without the motion. What gives?</p>
<p>Turns out, <em>computing the width of the popup button</em> is very slow. That requires measuring the width of every option in the menu, to make the button fit the widest one<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>. It seems that doing these measurements in SwiftUI is very slow; this was the case both for the native <code>Picker</code> view, and our own fake recreation, which goes through the same process<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>. What to do?</p>

<h3 id="parti-no-hover-no-picker">Part I: No hover, no picker</h3>
<p>If <code>Picker</code> is slow, let’s avoid rendering it.</p>
<p>Now, when you scroll through the list in Dye, no picker is actually there; just the fake button, the appearance. Only once you <em>hover</em> the button does the <code>Picker</code> get created, before you have a chance to click it. In practice, the interaction works exactly the same, but scroll performance is dramatically improved, since we skip the repeated measurements. Simple and effective.</p>
<pre><code class="language-swift">struct FakePicker&lt;FakePickerContent: View, PickerContent: View&gt;: View {
	@State private var shouldRenderPicker = false
	[...]
	
	var body: some View {
		fakePickerButton
			.onHover { isHovering in
				guard isHovering else { return }
				shouldRenderPicker = true
			}
			.overlay(alignment: .trailing) {
				if shouldRenderPicker {
					actualPicker
						.labelsHidden()
						.opacity(0.1)
						.opacity(0.1)
						.opacity(0.1)
				}
			}
			.accessibilityRepresentation {
				actualPicker
			}
	}
}
</code></pre>
<aside>
	(See <a href="https://codeberg.org/Cykelero/Dye/src/commit/5d64860a261a3f310e0238a73b16199cf8744c4a/Dye/Support/Views/FakePicker.swift">the actual implementation</a> for the full, unexciting details.)
</aside>

<h3 id="partii-designated-sizer">Part II: Designated sizer</h3>
<p>Now, for the final trick.</p>
<p>Scrolling is still slow, because we still create our fake popup button for every rendered row, which means we’re repeatedly measuring all ten options. There’s no way around rendering that fake button, but! Its width is the same in every row; why do we keep recomputing it?</p>
<p>There’s plenty of ways of computing that width once, then using the cached result in every fake picker. You could even make the argument for a hardcoded value, since macOS doesn’t have a system-wide font size setting, and Dye is currently only available in a single language. Feel free to imagine your own, reasonable solution; or keep reading for an overkill one.</p>
<p>The idea here is that the fake pickers’ content views are still responsible for computing their width, except one of them is randomly selected to make the measurement, sharing the result with everyone else. To achieve this, there’s a new <code>.minSizeWithSharedCache</code> modifier on the fake picker content, and a <code>.sharedMinSizeCache</code> modifier at the root of the app. They look like this in practice:</p>
<pre><code class="language-swift">// Using .sharedMinSizeCache: it coordinates everything
struct ContentView: View {
	var body: some View {
		[...]
		.sharedMinSizeCache()
	}
}

// Using .minSizeWithSharedCache: it communicates with the root cache
struct AccentColorPicker: View {
	@Binding var selection: AccentColor
	
	var body: some View {
		FakePicker {
			// The fake, visible picker button
			pickerOption(for: $selection.wrappedValue)
				.minSizeWithSharedCache(alignment: .leading) {
					ForEach(AccentColor.allCases) { accentColor in
						pickerOption(for: accentColor)
					}
				}
		} picker: {
			// The actual picker (invisible, but clickable)
			Picker(&quot;Accent color&quot;, selection: $selection) {
				[...]
			}
		}
	}
}
</code></pre>
<p>How do these work? It’s an easy 5-step process of mad back-and-forth.</p>
<ol>
<li>Each <code>.minSizeWithSharedCache</code> chooses a random <abbr>UUID</abbr> for itself, and propagates it up through a preference.</li>
<li>The root <code>.sharedMinSizeCache</code> receives all the <abbr>UIID</abbr>s, and picks one arbitrarily. It propagates the chosen <abbr>UIID</abbr> back down the tree through the environment.</li>
<li>The <code>.minSizeWithSharedCache</code> that got chosen computes the minimum size, by measuring its content view. It then sends that size back up the three through a preference.</li>
<li>At the root, <code>.sharedMinSizeCache</code> receives the measured size, caches it in its state, and sends it back down through the environment.</li>
<li>Finally, all <code>.minSizeWithSharedCache</code> read that value, and use it as their minimum size. Goal achieved: the size was computed just once, but enforced in every row.</li>
</ol>
<p>This was honestly very entertaining code to write! I think it’s especially fascinating how it’s essentially imperative code, expressed through declarative modifiers. <a href="https://codeberg.org/Cykelero/Dye/src/commit/7783f7adc78787bf6ed371677002797a632af880/Dye/Support/Views/minSizeWithSharedCache.swift">It’s an absolute mess to read</a>, because on top of that mismatch between behavior and expression, respecting view tree constraints forced an unnatural order for everything. Glorious, and dismal.</p>
<aside>
	(The modifiers are quite limited: they don’t react to width changes, and there’s no way of having <em>multiple</em> cached widths—every single instance of the modifiers talks to everyone else! Just fine for Dye’s current needs, but could use some work to be more generic.)
</aside>

<h2 id="in-conclusion">In conclusion</h2>
<p>I’m not sure how to convey this through words without sounding sarcastic. Doing all this work—reimplementing the picker appearance, creating a <code>Rasterize</code> view, lazy-rendering the actual control, and creating an over-the-top cache network—was a ton of fun! Engrossing engineering work, solving puzzles with unusual tools.</p>
<p>And at the same time, it’s a maddening amount of work for such a basic user interface. A naive AppKit implementation would have achieved excellent performance with little effort. SwiftUI, at the ripe old age of 6, just shouldn’t struggle with such basics. To me, the framework is built on remarkable foundations<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>, but is as a whole deeply immature, somehow starved for development resources. It’s delicious jam spread too thin.</p>
<p>Oh well; there’s always next <abbr>WWDC</abbr>.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>“Measuring text is slow” sure is recurring theme. <a href="/making-retcon-fast-caches/#rigorously-not-measuring-text">Last time</a> was about thousands of lines, though, not eleven words.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Jason Gregori <a href="https://hachyderm.io/@jasongregori/116216605877936999">points out on Mastodon</a> that most of what makes the  <code>FakePicker</code> slow to measure is our <code>Rasterize</code> view. This is in strange contrast to the  native <code>Picker</code>, which seems slow even when only containing text.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>SwiftUI’s core concepts are both excellently designed, and widely misunderstood. <a href="https://www.swiftuifieldguide.com/workshops/">I happen to teach their secrets</a>, though!&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]>
    </content>
  </entry>
  
  <entry>
    <author>
      <name>Nathan Manceaux-Panot</name>
      <uri>https://cykele.ro/</uri>
    </author>
    <id>tag:pending.design,2026:/swiftui-memoization/</id>
    <link rel="alternate" href="https://pending.design/swiftui-memoization/" />
    <title>Memoizing computed properties in SwiftUI</title>
    <published>2026-01-19T14:59:02+01:00</published>
    <updated>2026-01-19T14:59:02+01:00</updated>
    <summary type="text">A ready-to-use property wrapper.</summary>
    <content type="html" xml:base="https://pending.design/" xml:lang="en">
      <![CDATA[
<p>The SwiftUI view I was writing had gotten too slow. Any time it rendered, it spent a long time calculating one of its computed properties:</p>
<pre><code class="language-swift">struct ContentView: View {
	var input: Int
	var unrelated: Int
	
	var expensive: Int {
		input * 2 // let’s pretend that multiplication is super slow
	}
	
	var body: some View {
		Text(&quot;Expensive: \(expensive)&quot;)
		Text(&quot;Unrelated: \(unrelated)&quot;)
	}
}
</code></pre>
<p>Sure, whenever <code>input</code> changed, that expensive multiplication had to be recalculated; after all, I was using <code>expensive</code> in the view body. But if only <code>unrelated</code> had a new value? The view really ought to skip the <code>expensive</code> computation, and reuse the previous result.</p>
<p>You’d think there was a built-in way to perform this sort of caching, this <em>memoization</em>; or a ready-to-use code snippet online; but I couldn’t find any. So here’s my take!</p>
<h2 id="using-memoize">Using <code>@Memoize</code></h2>
<p>After copying the source (see below) into your codebase, using it only requires two changes:</p>
<ol>
<li>Add a <code>@Memoize</code> property, with the computed value’s type. This stores the cache.</li>
<li>In the expensive getter, wrap the computation with a call to the newly-added property wrapper, passing all the input values as arguments.</li>
</ol>
<pre><code class="language-swift">struct ContentView: View {
	var input: Int
	var unrelated: Int
	
	@Memoize() private var expensiveCache: Int // 1. Add this
	
	var expensive: Int {
		_expensiveCache(input) { // 2. Wrap in that
			input * 2
		}
	}
	
	var body: some View {
		Text(&quot;Expensive: \(expensive)&quot;)
		Text(&quot;Unrelated: \(unrelated)&quot;)
	}
}
</code></pre>
<p>That’s it! Now, whenever the <code>expensive</code> property is accessed, it’ll first check the cache for an up-to-date value, skipping the closure execution if none of the inputs changed. If there are expensive calculations in some of your views, this can make quite the difference.</p>
<p>Do make sure you list all the input values (here, just the <code>input</code> variable, but you can pass as many as needed). Otherwise, the output value won’t update when it should. The only requirement for these inputs is <code>Hashable</code> conformance.</p>
<p>For types that can’t conform to <code>Hashable</code>, or hash too slowly, you can pass a single <code>Equatable</code> value instead. To enable this, you’ll first need to tell the property wrapper about the key type: <code>@Memoize(key: SomeInput.self)</code>. The downside is increased memory usage, as the full input value will be stored as a cache key, instead of just a tiny hash.</p>
<h2 id="the-source">The source</h2>
<p>Download the ready-to-use <a href="Memoize.swift" download>Memoize.swift</a>
 file, or read the code and implementation notes below if you’re curious.</p>
<h3 id="memoizeswift">Memoize.swift</h3>
<pre><code class="language-swift">@propertyWrapper
struct Memoize&lt;Key: Equatable, Value&gt;: DynamicProperty {
	var wrappedValue: Value {
		fatalError(&quot;To use @Memoize, call it as a function, passing the input value(s), and a closure to produce the value&quot;)
	}
	
	@State @Boxed private var cache: (key: Key, value: Value)?
	
	init(key keyType: Key.Type = Int.self) {}
	
	private func getMemoized(key: Key, produceValue: () throws -&gt; Value) rethrows -&gt; Value {
		if let cache, cache.key == key {
			return cache.value
		}
		
		let newValue = try produceValue()
		cache = (key: key, value: newValue)
		return newValue
	}
	
	func callAsFunction(_ key: Key, produceValue: () throws -&gt; Value) rethrows -&gt; Value {
		return try getMemoized(key: key, produceValue: produceValue)
	}
	
	func callAsFunction(
		_ key: any Hashable...,
		produceValue: () throws -&gt; Value
	) rethrows -&gt; Value where Key == Int {
		var keyHasher = Hasher()
		
		for keyPart in key {
			keyHasher.combine(keyPart)
		}
		
		return try getMemoized(key: keyHasher.finalize(), produceValue: produceValue)
	}
}

@propertyWrapper
fileprivate class Boxed&lt;Wrapped&gt; {
	var wrappedValue: Wrapped
	
	init(wrappedValue: Wrapped) {
		self.wrappedValue = wrappedValue
	}
}
</code></pre>
<h3 id="implementation-notes">Implementation notes</h3>
<ul>
<li>Yes, that’s a box in a state. The state is necessary because we want the cache to persist across renders—it’d be pointless otherwise. But, we need to update that state during view updates, which isn’t supported by SwiftUI. To get around this, we wrap the cache in a reference type—that’s the box—enabling us to update it at any time, without ever modifying the actual contents of the state itself.</li>
<li><code>Memoize</code> is a <code>DynamicProperty</code>: this allows it to use SwiftUI property wrappers, such as <code>@State</code>.</li>
<li>The unusually-implemented <code>wrappedValue</code> is only there to help set the value type. It lets us type <code>@Memoize() private var expensiveCache: Int</code> instead of <code>@Memoize(value: Int.type) private var expensiveCache</code>. Arguably nicer.</li>
<li>Those empty parens in the <code>@Memoize()</code> call are necessary: it seems that without them, Swift doesn’t use our explicit init, and therefore skips the default key type.</li>
<li>To make calls to the memoization function more concise, we’re using <code>callAsFunction</code>, and an <code>any Hashable...</code> variadic parameter. Also, the wrapper takes care of hashing these input values, so you can have more than one input without needing to group them yourself, like you would for instance with <code>onChange(of:)</code>.</li>
</ul>
<p>Hopefully you find this bit of code useful. If you do try it, I’d love to hear your feedback!</p>
]]>
    </content>
  </entry>
  
  <entry>
    <author>
      <name>Nathan Manceaux-Panot</name>
      <uri>https://cykele.ro/</uri>
    </author>
    <id>tag:pending.design,2025:/retcon-icon-tahoe-update/</id>
    <link rel="alternate" href="https://pending.design/retcon-icon-tahoe-update/" />
    <title>Updating Retcon’s icon for Tahoe</title>
    <published>2025-12-12T10:12:33+01:00</published>
    <updated>2025-12-12T10:12:33+01:00</updated>
    <summary type="text">New constraints, new inspiration.</summary>
    <content type="html" xml:base="https://pending.design/" xml:lang="en">
      <![CDATA[<p>Icon design is a surprisingly pragmatic process. It’s all about finding concrete solutions, to issues you encounter while trying to reach specific goals. (and also you want to make things very pretty)</p>
<p>Updating <a href="https://retcon.app/">Retcon</a>’s icon for macOS 26 Tahoe involved a lot of trial and error, a lot of resolving (and creating) problems. So I recorded this short tour of some of the versions the icon went through, and the reasoning for everything. Give it a watch!</p>

<div style="position: relative; padding-top: 95.21%;"><iframe title="Updating Retcon’s icon for Tahoe" width="100%" height="100%" src="https://spectra.video/videos/embed/h7k3Kz4U84LqnwLDdLXjFm" style="border: 0px; position: absolute; inset: 0px;" allow="fullscreen" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe></div>


<p>If you enjoy this walkthrough, you’ll also like reading about <a href="/retcon-icon-process/">the making of the original icon</a>.</p>
]]>
    </content>
  </entry>
  
  <entry>
    <author>
      <name>Nathan Manceaux-Panot</name>
      <uri>https://cykele.ro/</uri>
    </author>
    <id>tag:pending.design,2025:/making-retcon-fast-caches/</id>
    <link rel="alternate" href="https://pending.design/making-retcon-fast-caches/" />
    <title>Making Retcon fast: A cache for every need</title>
    <published>2025-10-15T15:41:54+02:00</published>
    <updated>2025-10-15T15:41:54+02:00</updated>
    <summary type="text">Big speedups through bespoke caches.</summary>
    <content type="html" xml:base="https://pending.design/" xml:lang="en">
      <![CDATA[
<p>Retcon 1.0 was decently fast in modestly-sized repositories, but <em>appallingly</em> slow for anything big. You’d have to watch the app freeze for an agonizing 8.82 seconds to merely rename a commit in the Swift repo. <a href="https://indieapps.space/@Retcon/115056221545845136">In Retcon 1.4</a>, that same rename now completes in 448 forgettable milliseconds; a 1868% improvement. What’s the secret there?</p>
<p>Well, there’s no single thing. Months of work resulted in numerous, surprisingly diverse optimizations; some expected, some eyebrow-raising. Despite that variety, however, one kind of optimization ended up the most common by far: caches.</p>
<aside>
	(If you’re unfamiliar with Retcon, it&rsquo;s a new kind of Git client for macOS. It lets you manipulate your commit history with ease, removing all friction to creating meaningful, useful commits. More on <a href="https://retcon.app/">the Retcon website</a>.)
</aside>

<h2 id="many-values-many-caches">Many values, many caches</h2>
<p>Retcon now has numerous in-memory caches. Anything that’s slightly expensive to compute is stored in memory, to be reused the next time it’s needed. To make sure these cached values don’t go out of date, they’re either discarded whenever the input data changes, or are stored alongside a fingerprint of the input data, to allow checking validity on retrieval.</p>

<p>One of Retcon’s most important classes is <code>VirtualizedRepository</code>. <abbr>VR</abbr> objects mediate access to Git repositories, while transparently layering Retcon’s own data on top. They’re core to allowing Retcon to display and manipulate states that Git itself can’t represent, such as a commit histories with a conflict midway through.</p>

<p>Because of this, a <abbr>VR</abbr> exposes many different properties, most derived from on-disk data, that get accessed quite heavily. And so these properties are cached: the head commit list is cached as an array, allowing for random access, instead of requiring walking the history one commit at a time through parent lookups. The repository’s tags are cached in a purpose-made map, <code>cachedTagsBySHA1</code>, that makes it very fast to look up a given commit’s tags, instead of needing to filter through the repository’s complete tag list every time. There’s the precisely-named <code>cachedPhysicalSecondaryHeadAncestorsByMergeCommitLineage</code>—a map of the commits that were merged into the current branch, keyed by the identity<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> of their merge commit. Purpose-made, once again. And there’s even a cache of the behind/ahead counts, that are eventually displayed on the pull/push buttons—if your branch has significantly diverged from its upstream, finding the closest shared parent can take a while.</p>
<p>Many of the <abbr>VR</abbr> caches are regenerated from scratch whenever any repository data changes, so that they’re always up to date. But some are handled with more granularity, for performance.</p>
<p>The head commit list cache is updated, not discarded. When a repo changes, the vast majority of the history usually stays the same; so Retcon holds onto the current cache’s tail of unchanged commits, and prepends new and modified commits<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>.</p>
<p>The merged commit map is never proactively built, but instead gets populated as data is requested. This is because in Retcon, merge commits are initially displayed collapsed: their child list is only needed once the user toggles them open.</p>
<h2 id="rigorously-not-measuring-text">Rigorously not measuring text</h2>
<p>Caches are useful for more than abstract, internal state. Computing the layout of <abbr>UI</abbr> elements can also be expensive, once again inviting caches; Retcon’s best example is the measuring of diff hunk heights.</p>
<aside>
	(In Git diffs, modified files aren’t displayed in their entirety, but are instead shown as a series of <em>hunks:</em> snippets from the file, focused on a few adjacent changed lines, with some surrounding context. A diff may contain just one such hunk, or many of them; each hunk can range from a handful of lines, to thousands or more.)
</aside>



<figure class="no-frame"><picture>
    <source srcset="hunks-in-retcon-dark.png"
      alt="Screenshot from Retcon, the macOS Git client, showing the diff view. Two HTML files are displayed. The first one with a single hunk, with a single change; the second file has two hunks, which have disjoint line numbers." width="832" height="589"media="(prefers-color-scheme: dark)"
    >
    <img src="hunks-in-retcon-light.png"
      alt="Screenshot from Retcon, the macOS Git client, showing the diff view. Two HTML files are displayed. The first one with a single hunk, with a single change; the second file has two hunks, which have disjoint line numbers." width="832" height="589">
  </picture><figcaption>
      <p>Three hunks, from two different files.</p>
    </figcaption>
</figure>

<p>Now, while generating diff hunks is fast, computing their on-screen height is not. Retcon doesn’t let the text scroll horizontally, instead wrapping it at the window edge; this means that to know the total pixel height of a hunk, the layout system must scan through the entirety of its text, determining line wrapping points, and therefore the number of lines the hunk actually spans<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>. This is a very, very slow process for large amounts of text.</p>
<p>Which brings us back to caches. It’s a no-brainer to cache the output of this expensive measuring process, as a given hunk will often stay the exact same when the diff view is refreshed. Here’s the tricky part, though: what does it mean to be the <em>exact same</em>? If the hunk’s text remains unchanged, but the window becomes wider, then we should redo the measurement—more horizontal space means less wrapping. If one of the hunk’s neighbors changes in content, we ostensibly don’t need to reflow the unchanged hunk itself—unless the neighbor’s <em>line numbers</em> are now different, since these dictate the width of the line number gutter, which is shared by all of a file’s hunks. So many variables to take into account, coming from such distinct sources!</p>
<p>So we need to invalidate our cache based on these diverse factors, and do so with precision: while the height must never go out of date, we also want to avoid costly superfluous invalidations. To achieve this granularity, Retcon makes use of a supporting method, that fetches all the relevant inputs for a given hunk, and mixes them into a single integer:</p>
<pre><code class="language-swift">\showLineNumbersStartingFrom: 624
/// A hash of all the properties that have an impact on this hunk's view height.
func heightDependenciesHash(
	forHunkAtIndex index: Int,
	withParentWidth parentWidth: CGFloat
) -&gt; Int? {
	guard let viewHunk = viewHunks[safeIndex: index]
		else { assertionFailure(); return nil }
	
	var hasher = Hasher()
	hasher.combine(parentWidth)
	hasher.combine(contextIsDough)
	hasher.combine(effectiveForceDisplayingHunks)
	viewHunk.hunk.combineText(into: &amp;hasher)
	hasher.combine(hunkGutterWidth)
	return hasher.finalize()
}
</code></pre>
<p>The method essentially captures the complete context in which a hunk is measured, and represents it as a hash. This allows us to store the hash alongside the cached height, and use it as the cache key: whenever the cache is read back, we first check if the stored hash matches the current output of the method. If the two differ, it means the cached height is stale, and we need to take a fresh measurement.</p>
<p>The nice thing about this approach is that it neatly encapsulates all knowledge about hunk height calculation. While this listing of factors is itself duplication (after all, the window width will affect hunk height regardless of what we hash in the method), it does prevent duplicating and scattering that information any further. It’s the one, canonical way of representing hunk cache freshness.</p>
<h3 id="unrigorously-measuring-text">Unrigorously measuring text?</h3>
<p>In a slightly different world, we could actually skip measuring most hunks. If a hunk isn’t in view, all we need is an approximation of its height, so that we can properly size the scroll bar’s thumb.</p>
<p>Retcon has a way of producing such an estimate: it maintains <code>estimatedRowHeightToLineCountRatio</code>, a cache of the average ratio between the pixel height of a hunk, and the number of line break characters it contains, based on hunks measured so far. We always know how many line breaks a hunk has, so it’s extremely cheap to get an idea of its height using this value.</p>
<p>However, this technique turns out to be unusable. AppKit’s <code>NSTableView</code> class, that Retcon uses to display the list of hunks, requires us to provide <em>exact</em> heights, not estimates, when enumerating our hunks<sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>. If the values we provide are off by even a few pixels, the view will constantly stutter and jump when scrolled, making for a terrible experience. Oh well.</p>
<h2 id="the-long-tail">The long tail</h2>
<p>There’s many more caches in Retcon. They all bring welcome performance boosts, but most aren’t especially interesting. Here’s one last story for the road.</p>
<p>The welcome window, with its list of recently-opened repositories, could be slow to appear, sometimes stalling for more than a second. The view code would read the system-provided <code>recentDocumentURLs</code> list many times per refresh, with the assumption that the access would be instant; it was not. One <code>cachedRecentDocumentURLs</code> later, and the window now shows up without delay. A welcome improvement!</p>
<p>There’s a lot more to Retcon 1.4 than caches, though; we’ll look at the many other optimizations in future blog posts. Follow along on <a href="https://mas.to/@Cykelero">Mastodon</a>, <a href="https://bsky.app/profile/cykele.ro">Bluesky</a> or <a href="/feed.atom.xml">Atom</a>, and <a href="https://retcon.app/">get Retcon now</a> to see firsthand how fast it’s become.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>In regular Git, nothing links a rewritten commit to its source commit: they’re entirely unrelated. A commit’s hash completely changes any time it’s even slightly modified. Retcon works around this by assigning each commit a unique identifier called a <em>lineage <abbr>ID</abbr></em>, which it keeps stable across rewrites. This is primarily to allow animating large history changes, such as rebases, to make it much clearer what’s happening.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Would you know it: updating the head commit cache makes use of <em>another</em> cache, <code>cachedPhysicalHeadCommitIndicesBySHA1</code>, that’s maintained solely for this purpose.</p>
<p>To determine where the unchanged tail of the new history starts, Retcon needs to walk commits one by one, starting with the head, checking whether each is already contained in the cache. Such a lookup would be very slow if done naively (“for each new commit, iterate over the current cache to see if it’s in there”), so instead we’re using this map, which makes the lookup vastly faster.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>The text layout process, handled by Apple’s TextKit 2, also accounts for varying typographic bounds. While lines of only <abbr>ASCII</abbr> characters always have the same height, adding a Chinese character or an emoji will use a different font, often making the line taller. So even if we disabled line wrapping, calculating a hunk’s total height would be more complicated than just <code>hunkLinebreakCount</code> × <code>lineHeight</code>.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Retcon’s diff view implements the table view delegate method <code>tableView(_:heightOfRow:)</code>, which expects an exact height.<br>
In UIKit, <code>tableView(_:estimatedHeightForRowAt:)</code> exists precisely for communicating estimates like ours, but AppKit has no equivalent.&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]>
    </content>
  </entry>
  
  <entry>
    <author>
      <name>Nathan Manceaux-Panot</name>
      <uri>https://cykele.ro/</uri>
    </author>
    <id>tag:pending.design,2025:/clipboard-manager-uses/</id>
    <link rel="alternate" href="https://pending.design/clipboard-manager-uses/" />
    <title>The other uses of a clipboard manager</title>
    <published>2025-09-11T15:36:20+02:00</published>
    <updated>2025-09-11T15:36:20+02:00</updated>
    <summary type="text">Go beyond the obvious, and make your life nicer.</summary>
    <content type="html" xml:base="https://pending.design/" xml:lang="en">
      <![CDATA[<p>You know that clipboard managers are useful tools, but you might not know how useful. They’re for much more than swapping two variable names around—turns out, having the ability to <em>instantly persist any piece of text</em> gives you a lot of new abilities. So here’s a list that covers the obvious, then the creative.</p>
<h2 id="the-obvious">The obvious</h2>
<ul>
<li><strong>Text swapping</strong> becomes easy. Copy A, copy B; paste B over A, then recall A and paste it over B.</li>
<li><strong>Peace of mind.</strong> When your clipboard holds an important item you mustn’t discard, you no longer need to consciously stop yourself from copying anything. It’s OK if you forget and override that item; it’s OK to switch to a quick side-task that requires the clipboard, or to reboot your computer, or let someone else use it. Your clipboard is no longer <em>fragile</em>. To some people, this makes a world of difference; it’s one less thing to actively keep in mind.</li>
</ul>

<h2 id="the-creative">The creative</h2>
<p>That was the expected. Leaning on your clipboard manager, though, you can start using your computer differently.</p>
<ul>
<li><strong>Long recall.</strong> If you can remember manipulating some piece of text, there’s a chance you copied it. So if you can recall just a few unique letters from it, that clip can be instantly recalled, using the clipboard manager’s search feature.<br>
An email address you copied? Often, just type the person’s name. A URL you recently shared on a chat? The website name. That Terminal command to clear a file’s quarantine flag that you keep looking up online<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>? Try “quarantine” and it should be there.<br>
The list goes on! Without any upfront effort of your part, so much of the interesting text you manipulate can later be recalled in just a few keystrokes. The longer your clipboard history, the better, too; I’ve configured my clipboard manager to keep items for <em>months</em>, so if I remember something, it’s in there for sure.</li>
<li><strong>Instant backup.</strong> Ever lost text to a flaky Web form after spending half an hour composing it? Well, you now have the means to <em>instantly backup any piece of text:</em> <kbd>⌘A</kbd>, <kbd>⌘C</kbd>. That’s it! It’s so fast, you can do it all the time. And now you won’t ever again lose anything you’ve written. <kbd>⌘A</kbd>, <kbd>⌘C</kbd>. Your own instant, ubiquitous backup system.</li>
<li><strong>Regret-proof delete.</strong> This builds up on the previous item. Now, whenever I delete any piece of text that’s longer than a couple words, I do so using <kbd>⌘X</kbd>. That goes for code, or unsent chat messages, or Web searches. Most of the time this is pointless; but often enough, I’ll be happy to save some thinking and typing, when I realize that actually, that first draft <em>was</em> in the right direction. Anything you delete by cutting can be undeleted, and there’s no cost to doing it.</li>
<li><strong>Photographic memory.</strong> And it’s not just about text: I’ll often take a screenshot of a settings windows right before I tweak something. <kbd>⇧⌘4</kbd>, Space, then Control-click: I instantly captured a reference of how things were configured, before I went and messed them up. My future self often thanks me.</li>
</ul>
<p>So many uses out of that single simple system! I think that’s really cool. I hope you find these ideas useful; and if you have your own schemes, please do share them with me on <a href="https://mas.to/@Cykelero">Mastodon</a> or <a href="https://bsky.app/profile/cykele.ro">Bluesky</a>. I’ll be happy to use my clipboard manager even more.</p>
<h2 id="appendix-what-clipboard-manager-to-use">Appendix: What clipboard manager to use?</h2>
<p>The app I personally use does the job brilliantly; however, it stores all of its data unencrypted on the file system, and the developers have stated they didn’t intend on fixing the issue, so I’m really not comfortable recommending it.</p>
<p>Fortunately, there’s plenty of clipboard managers out there! Find one that’s got very fast search, and a large maximum history size, and you’ll be set.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><code>xattr -rds com.apple.quarantine FILE_PATH</code>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]>
    </content>
  </entry>
  
  <entry>
    <author>
      <name>Nathan Manceaux-Panot</name>
      <uri>https://cykele.ro/</uri>
    </author>
    <id>tag:pending.design,2025:/stories-of-making/</id>
    <link rel="alternate" href="https://pending.design/stories-of-making/" />
    <title>Stories of making</title>
    <published>2025-07-30T16:18:25+02:00</published>
    <updated>2025-07-30T16:18:25+02:00</updated>
    <summary type="text">I finally have a blog!</summary>
    <content type="html" xml:base="https://pending.design/" xml:lang="en">
      <![CDATA[<p>I’ve always loved reading stories of people making things. The clever displays of creativity; the intoxicating sense of <em>possibility</em> that they evoke. They make the world feel more open-ended.</p>
<p>I <em>also</em> really like telling people all about the things I’m making. There’s a lot of pleasure in describing, in great detail, the goals, the obstacles, the breakthroughs, even the vexing compromises.</p>
<p>Up until now, though, it’s mostly been my friends’ ears that I’ve been talking off. With this blog now online, it can finally be yours too!</p>
]]>
    </content>
  </entry>
  
  <entry>
    <author>
      <name>Nathan Manceaux-Panot</name>
      <uri>https://cykele.ro/</uri>
    </author>
    <id>tag:pending.design,2023:/retcon-icon-process/</id>
    <link rel="alternate" href="https://pending.design/retcon-icon-process/" />
    <title>Creating Retcon’s icon</title>
    <published>2023-11-23T12:00:00+02:00</published>
    <updated>2023-11-23T12:00:00+02:00</updated>
    <summary type="text">The process of making a Mac app icon.</summary>
    <content type="html" xml:base="https://pending.design/" xml:lang="en">
      <![CDATA[
<aside>
	(This article was originally published on Dribbble. It was imported on this blog on December 12th, 2025.)
</aside>

<figure><img src="/retcon-icon-process/hero.png"
    alt="The app icon of macOS app Retcon. It depicts a small whiteboard, with a git branch symbol drawn on it. A marker is placed on top. The symbol, marker, and the corners of the board are purple." width="800" height="600">
</figure>

<p>This is the icon for <a href="http://retcon.app/">Retcon</a>, the macOS app for rewriting git history at the speed of thought. My first Mac icon! Rendered in Sketch, with reference renders in Blender.</p>
<p>I followed a process described in <a href="https://bjango.com/articles/macappiconworkflow/">an article from Marc Edward</a>: start with concept sketches, create a 3D version of your icon as a reference, then render the final icon in vectors.<br>
The 3D reference allows the icon to have realistic shapes and lighting—as seen on the pen—but the vector re-render means the icon&rsquo;s look can be fully controlled; great for aesthetics, and legibility.</p>
<h2 id="concept">Concept</h2>
<p>Retcon allows you to modify git history very fast, with the same ease you&rsquo;d move a folder in the Finder, or rename a file. That&rsquo;s what motivated many of the concepts: splicing a movie, tearing pages from a book, redacting a page, etc. The app&rsquo;s all about fast editing.</p>
<p>Ultimately, the whiteboard felt like the clearest metaphor. A close second was the blackboard, but while that might have made for a more visually pleasant icon, it also felt less modern, which wasn&rsquo;t the appropriate for the app.</p>
<p>Some of my favorite concepts are the whiteboard titled “Progress”, and the blackboard being erased; but, while they&rsquo;re pretty compelling as rough sketches, these concepts would probably have been much too busy as actual icons.</p>
<figure><img src="/retcon-icon-process/concepts.jpeg"
    alt="A series of rough sketches depicting various potential app icons: whiteboards, books, a calendar, etc, with different tools on top (a pen, a pencil, a marker, various erasers), and a bunch of miscellaneous symbols." width="800" height="450">
</figure>

<h3 id="symbol">Symbol</h3>
<p>The whiteboard features a git “branch” icon, which feels like a rather obvious choice in retrospect. It immediately ties the icon to git, and slightly hints at history and branch manipulation.</p>
<p>I explored many symbols, though, before settling on this one. Most notably, I looked for a less direct depiction of a git history; something more concrete and relatable, like the tree-related symbols. I eventually decided that the absolute clarity of the actual symbol for a git branch worked perfectly, in the context of the icon. In Retcon, you&rsquo;re literally erasing and rewriting branches!</p>
<p>Another very tempting choice was Apple&rsquo;s “A” app symbol. It evokes development very clearly, and has strong (and hopefully positive) associations.<br>
However, it evokes <em>app</em> development, rather than software development in general, which is too specific for Retcon; and it feels owned by Apple, at least in culture, if not as a trademark.</p>
<h2 id="a-vector-previz-and-a-3d-render">A vector previz, and a 3D render</h2>
<p>These really helped nail down the icon&rsquo;s layout, scale, and overall look. When then rendering the final icon, I heavily referenced the pen, for realism, but mostly made up the board, which wasn&rsquo;t modeled very interestingly in the 3D model.</p>
<figure><img src="/retcon-icon-process/previsualization-and-reference-render.png"
    alt="A bunch of unfinished drawings and 3D renders of the Retcon icon." width="800" height="600">
</figure>

<h2 id="final-render">Final render</h2>
<figure><img src="/retcon-icon-process/final-render.png"
    alt="A bunch of unfinished versions of the Retcon icon." width="800" height="600">
</figure>

<h2 id="balancing-viewing-scales">Balancing viewing scales</h2>
<p>Although Mac icon files can contain many variations, that the system then selects from depending on icon display size, I decided to only create a single size. (or perhaps forgot that you could create these variations. events unclear!)</p>
<p>That meant the icon had to work both when displayed large (as a marketing asset, or when inspecting by the user using Quick Look) and small (in the Dock, in the Finder).</p>
<p>Of the two, the smaller size was definitely the more important one, being the icon&rsquo;s usual display scale. That meant sacrificing good-looking small details, if they muddied the icon when viewed small.</p>
<p>You can see this tension in action in the two-up comparison below. A WIP shot is on the left, and the final icon is on the right.<br>
The final icon is coarser, its various borders thicker, its shadows heavier. The end result is a lot less elegant when viewed up close, but vastly more legible when viewed at Dock size. The process of removing subtlety was painful, but ultimately made for a much better icon!</p>
<figure><img src="/retcon-icon-process/scale-comparison.png"
    alt="A comparison of two versions of the Retcon icon; each version is showed in full scale, and at a small scale. The version on the left has much finer features and details; its rendering is more subtle. The version on the right, however, is much nicer to look at from afar: its details remain visible, whereas the left version becomes muddy and hard to interpret when small." width="800" height="600">
</figure>

<h2 id="objectives">Objectives</h2>
<p>Going in, I had objectives in mind; some essential, some optional. The icon definitely doesn&rsquo;t fulfill them all, but compromises are core to design, no?</p>
<p>The essential goals are predictable:</p>
<ul>
<li><strong>Be a functional icon:</strong> Be recognizable, legible at small sizes, and evocative of the app itself.  <span style="font-size: 0.65em; padding: 0 2px 0 3.5px; vertical-align: 2px">◆</span> 
 That one&rsquo;s a go! As the most important goal, it forced quite a few decisions, as described above.</li>
<li><strong>Look at home on macOS:</strong> The system has its own design language. It&rsquo;s a rather strongly-defined one, although there&rsquo;s still a lot of leeway for experimentation.  <span style="font-size: 0.65em; padding: 0 2px 0 3.5px; vertical-align: 2px">◆</span> 
 I think the icon does just fine here. It&rsquo;s realistic but not actual 3D, and respects the round rect while playing with it a little. The pen really helps, too—more on that below.</li>
</ul>
<p>The optional goals were a bit more personal:</p>
<ul>
<li><strong>Use varied materials:</strong> While assembling a board of reference material, I kept encountering app icons that felt nice, but… were somehow just short of great. Eventually, I realized that they often fell into the trap of having every component rendered the same way: the whole icon would look like a plastic model of a thing, instead of looking like the thing itself. To counter that effect, I really wanted the icon to feature different materials, of which the different properties would contrast in the final render.  <span style="font-size: 0.65em; padding: 0 2px 0 3.5px; vertical-align: 2px">◆</span> 
 I don&rsquo;t think I did an excellent job, here. There&rsquo;s certainly diversity: slightly rough metal for the board frame, shiny white plastic for the pen&rsquo;s body, and some variations of plastic and paper. However, especially when viewing the icon at a small scale, the material differences barely stand out; only the metal frame&rsquo;s telltale shine makes it stand out from what otherwise looks all-rubber, or all-plastic.</li>
<li><strong>Include a tool:</strong> Some of my favorite Mac apps have a tool on their icon. It&rsquo;s an old convention, that screams “this app will let you <em>make</em> things”. I love creation apps, and I love this association—so it was important to me for Retcon&rsquo;s icon to feature a tool.  <span style="font-size: 0.65em; padding: 0 2px 0 3.5px; vertical-align: 2px">◆</span> 
 I&rsquo;m happy I got to do this one! Luckily, the best concepts for the icon all made including a tool natural, if not necessary. The most observant will notice that the pen is shifted right from the standard, Big Sur-style tool location, but the appropriate 24° angle is respected!</li>
<li><strong>Show ample color:</strong> It&rsquo;s so nice to look at something colorful. I wanted the icon to have a well-defined dominant color, and feature it prominently, with a large portion of the icon being painted.  <span style="font-size: 0.65em; padding: 0 2px 0 3.5px; vertical-align: 2px">◆</span> 
 Well, that one&rsquo;s a wash—the icon does have a clearly-communicated tint, but it&rsquo;s not used over large areas; instead, the most represented color by far is classic <em>whiteboard white</em>. Gotta pick your battles!</li>
</ul>
]]>
    </content>
  </entry>
  
</feed>
