The Rendering Model
Why filedom never changes an icon in place: Finder's blanking behavior, the move-only law, and the park + cover protocol.
The unusual parts of filedom’s component API all follow from one Finder behavior. Read this page before the component pages.
The core constraint
Finder repaints a moved file instantly. When a file’s custom icon changes in place, or when a file is created at a visible spot, Finder shows a blank or generic icon for one to two seconds before the real artwork appears. There is no reliable way to force an immediate repaint.
A static drawing can live with that. A game running at 10 to 60 ticks per second cannot: a snake head that swaps its icon every tick would be a constant gray flicker.
The move-only law
Every filedom component obeys this rule:
Anything whose appearance changes during play is modelled as a fixed set of pre-baked files that are only ever moved. An icon is set once, at file creation, and never changes again.
A score is ten files per digit place (digits 0 to 9). The current digit is moved into view and the other nine stay hidden. A “YOU WIN” banner is created at load and hidden; moving it to the center is how it is shown.
Hidden sprites still need somewhere to live.
The park and the cover
Finder clamps icons onto the desktop, so off-screen coordinates don’t work. filedom hides sprites in plain sight instead.
The park is one point, usually a corner, where every hidden sprite sits stacked. A sprite is shown by moving it from the park to a display position and hidden by moving it back.
The cover is a borderless window owned by the icon daemon. It is filled with the
desktop background colour and sized to the park’s icon cell plus the label band
under it. It sits at window level desktopIconWindow + 1, above Finder’s whole
icon layer and below every app window, so the parked pile looks like empty
desktop whatever Finder does to icon z.
Two properties make this work:
- The cover is ordered by the window server. It is not an icon, so no Finder z law applies to it, and later creations, unhides, drags, or clicks cannot bring the pile above it.
- Every sprite warms behind the cover. Pool files are created hidden, positioned at the park while hidden, and unhidden in place at the park. A sprite is not revealed until 1.5 s after the native unhide is confirmed, the time Finder takes to paint its real icon over a placeholder, so the warm-up happens entirely under the cover. The 1.5 s is a measured allowance. Finder gives no paint acknowledgement, so failures and incomplete cache entries still need diagnosis.
The Stage components implement this protocol. Games describe faces and positions in JSX, and each frame becomes a set of moves.
When a stage grows, the new file is created hidden at the park like every other
file and warms behind the cover. A complete prebake still avoids creating
files mid-game, but it only sizes pooled sprites. A static sprite’s file is
minted when the sprite mounts; for chrome mounted at boot, that is the same
first commit in which a prebake warms.
The four z-order laws
These are empirical results from spike tests in the repo.
Z is creation recency
Finder stacks coincident desktop icons with the newest file on top. No API sets z on an existing file.
Within one commit the renderer creates files in child order, because the
filesystem lane is a serialized chain in commit order. The order sprites are
written in is the order their files are created, and therefore their z. That
only helps a sprite that owns its file; a pooled sprite may be handed any warm
file with its look. This is why text tiles and other overlapping chrome are
static sprites.
Name shape is a z-order input
Creation recency only holds when files are named with a specific encoding: width 14 over exactly two characters, U+2009 THIN SPACE and U+200A HAIR SPACE, with a sequential index written most-significant-first. The shape was found experimentally.
A 65-file game-shaped spike with full run forensics measured the alternative: with fixed-width-8 names over a 12-character invisible alphabet, Finder put the newest file under older ones even though creation order, positions, and icons were all correct, and parked sprites leaked into view. The same run with the two-character encoding, at any base offset, stacked correctly. The alphabet and width were the only variables.
Within the encoding, the index does not decide z; creation recency does. The encoding is a precondition for creation recency to work at all.
The Stage’s per-root name registry mints these names, so you never write them yourself.
Keep them byte-exact. Trimming a selection path removes its real basename, and trimming a whole native TSV response can remove the first row’s filename. filedom’s parsers strip protocol framing only. Never replace the encoding or normalize the whitespace as a cleanup or optimization.
Z is sticky per path
Deleting a file and creating a new one at the same path restores the old
stacking; the new file does not come out on top. Creating onto an existing path
is also z-inert, because the file keeps its existing st_birthtime. Recreating
a file for a fresh z only works under a path that has never been used.
The library does two things about this. The name registry starts each run at a random base offset inside the encoder’s capacity (only the offset is random; the counter above it is monotonic). And the reconciler sweeps a previous run’s abandoned invisible files at boot, because a leftover is a stray icon that Finder arranges wherever it likes, outside the park cover.
Fresh creation is another z-order lever
A new path gets a new birthtime and lands on top. This is the mechanism behind on-demand sprites, and it costs one create round-trip per file.
What does not change z
- Programmatic moves (
setIconPosition) do not change z, which keeps the park stable. - No in-place change does either. All of these tested negative: mtime bumps,
re-applying a custom icon, rewriting the file’s content,
chmod, Finder comments, tags, and renames. - A real pointer drag is the exception. Finder raises a hand-dragged icon above everything it overlaps, permanently.
Stage also repairs overlapping ranked sprites with a selection plus a synthetic
drag. A single hand-dragged file rises, but programmatically moved followers do
not. Set the ordering with the Sprite props
zOrder and stackingRegions.
The drag exception matters for drag-driven games such as solitaire and chess: a
card the user dragged stays above every move-only overlay. A static file does
not help, because it is minted at mount and is older than the drag. Use
<Sprite onDemand />, or
<Text onDemand />, which creates fresh files on each show. A dense grid
Text panel avoids the problem a different way, by
anchoring the help clear of the play area.
What this rules out
These approaches were all tried and rejected:
- Swapping a file’s icon at runtime (a 1 to 2 second blank every time).
- Creating files mid-game at visible positions (a blank at the worst moment; a new file must warm hidden at the park before it is shown).
- Hiding icons off-screen (Finder clamps them back).
- Forcing repaints with touch, rename, or comment tricks (z-inert, and some blank).
For anything animated, use a Stage and let it own the protocol.