Files
Sylpheed/docs/re/structures/ui-button-focus-record.md
Sylpheed RE agent b21c8e4118 re(ui): the focus ring's position is decoded -- a .rat leaf parses as a build
The port needed ptbtneff01.t32's placement and was about to author it from an
eyeballed PNG measurement. It does not have to: a `.rat` leaf needs no new
reader. Its first 32 bytes have a bundle header's shape -- "RATC", 0x3c
declaration-entry size at +4, element count at +20, design 1280x720 at +24/+28 --
so ui_layout::parse_build reads it unchanged.

The control is the base record, whose position is known independently: the parent
screen reports ptbtn01.rat resting at (542,162), and parsing the leaf alone
returns ptbtn01.t32 at (542,162). It reproduces all five buttons.

Positions are absolute design-space top-left. The ring rests at (500, 156/236/
316/396/476) for buttons 1-5 -- a uniform (-42,-6) from each button's own rest,
identical in the Japanese bundle. The bright label is a uniform (-7,-7).

Two things recorded rather than smoothed over: a leaf's placement DUPLICATES the
parent's rather than being relative to it, and the two copies are not always
byte-equal (ptbtn04's parent says y=401, its leaf says 402) -- the parent is what
compose honours, so the leaf is the source only for elements the parent does not
declare, which is exactly the ring. And `screen render --focus` is blind to the
ring for the same reason the port's exporter was: el.focused is name-based on
top-level elements and neither walks into the leaf.
2026-08-29 08:36:53 +00:00

7.5 KiB
Raw Blame History

✅ A focused button is a two-element record, and the second element is the ring

Status: 🟡 DECODED for GP_TITLE, disc-wide check not yet run; the sprite contents and the ring are ✅ MEASURED against a live capture. Answers the port's "focus: drawn over the base element, or instead of it?"

⚠️ The element-count field at +20 is read here on GP_TITLE's ten button records only (five buttons × two language bundles). A disc-wide check — "does +20 equal the number of resource names the leaf embeds, for every .rat leaf on the disc?" — is written and was still running when this was committed. Until it lands, treat +20 as a strong local reading, not a decoded field: the observable claim below (the focus record carries a second sprite, ptbtneff01.t32, and the port must draw it) rests on the embedded names and the capture, not on that word.

Short answer: it does not matter, and that is not the bug. The focused sprite covers the base completely, so over-vs-instead is worth at most 12/255 on ~25 pixels. What is missing is the focus record's second element — a glowing ring that only exists when focused.

The record

A button's base and focused variants are both .rat leaves, and they are not the same shape:

ptbtn02.rat   164 B   strings: RATC, ptbtn02.t32,                  opt  ptbtn02f.rat
ptbtn02f.rat  312 B   strings: RATC, ptbtneff01.t32, ptbtn02f.t32, opt  ptbtn02b.rat

The header says so explicitly. A .rat leaf begins:

+0   "RATC"
+16  u32   flags        0x00008110 (base)   0x00008112 (focused)
+20  u32   ELEMENT COUNT      1                    2
+32  first element name

So the focused record declares two elements — ptbtneff01.t32 first, then ptbtn0Nf.t32 — where the base declares one. Both f records of both language bundles read the same way, and the flag word differs only in bit 0x02 alongside the count.

⚠️ The opt link on a .rat leaf is not simply "my focused variant". The chain runs ptbtn01.rat → ptbtn01f.rat → ptbtn02.rat → ptbtn02f.rat → ptbtn02b.rat, i.e. it threads base and focused records together in order. The corpus calls it the focus link and for a base record that reading works; do not generalise it to the f records.

The three sprites

Decoded with sylpheed-cli pak textures, shown over a checkerboard in button-base-focus-ring-sprites.png:

sprite size what it is
ptbtn02.t32 117×43 the dim label, its underline, and a small dot-in-circle at the underline's left end
ptbtn02f.t32 130×56 the same label, bright and glowing — a complete replacement, 13 px larger in each axis
ptbtneff01.t32 42×46 a glowing ring, focus only

The small dot-in-circle is on every button all the time; the large ring is the focus marker. They are different things and sit side by side on the focused row — visible in live-main-menu-options-focused.png against live-main-menu.png.

Over or instead? Measured, and the answer is "unobservable"

Aligning base and focused by normalised cross-correlation of their alpha masks — the true offset is (7,7), not the (6,6) that centring predicts —

pair base-visible px f alpha ≥ base alpha there
ptbtn02 / f (bundle b58a0fe6) 1 444 100.0 %
ptbtn01 / f (bundle b58a0fe6) 1 898 100.0 %
ptbtn02 / f (bundle a715f485) 3 720 100.0 %

⚠️ At the centre alignment it reads 78–84 %, and that number is an artefact. A 1 px shift on strokes this thin manufactures a fifth of a sprite's worth of "the focused art is thinner here". Solve the alignment before trusting a coverage figure.

Coverage is not the same as hiding, though, because f is not fully opaque everywhere. Compositing both ways over the menu's own background colour:

pair max channel difference px > 8/255 RMSE over the button rect
ptbtn02 12.5 23 1.09
ptbtn01 12.2 27 1.08
ptbtn02 (JP) 12.2 27 1.05

So the two hypotheses differ by ~1.1 RMSE inside the button rectangle, on a couple of dozen pixels — below the ≈ γ 1.4 tone gap (tone curve) and far below any frame-level RMSE this corpus can resolve. Either choice is defensible; neither is measurable. Replacing is the cheaper one and is what the file's structure suggests, since ptbtn0Nf.t32 is a whole label rather than an overlay.

✅ Where the ring is placed — DECODED 2026-08-29, no authoring needed

This page previously said the per-element placement inside a .rat leaf was not decoded and that a consumer should eyeball it off a capture. That was wrong by omission: a leaf needs no new reader. Its first 32 bytes have the same shape as a bundle header — "RATC", 0x3c declaration-entry size at +4, element count at +20, design size 1280x720 at +24/+28 — so ui_layout::parse_build reads it unchanged.

The control is the base record, whose position is known independently: the parent screen reports ptbtn01.rat resting at (542,162), and parsing the leaf on its own returns ptbtn01.t32 at (542,162). It reproduces all five.

Positions are absolute design-space top-left, not offsets (examples/rat_leaf_placement.rs):

button base ring ptbtneff01.t32 Δ label ptbtn0Nf.t32 Δ
ptbtn01 (542,162) (500,156) (−42,−6) (535,155) (−7,−7)
ptbtn02 (542,242) (500,236) (−42,−6) (535,235) (−7,−7)
ptbtn03 (542,322) (500,316) (−42,−6) (535,315) (−7,−7)
ptbtn04 (542,402) (500,396) (−42,−6) (535,395) (−7,−7)
ptbtn05 (542,482) (500,476) (−42,−6) (535,475) (−7,−7)

The offset is uniform: (−42,−6) for the ring and (−7,−7) for the label on every button, and identical in the Japanese bundle (pak entry 8).

⚠️ One 1-unit disagreement, and it is real. ptbtn04's parent element rests at y 401 while its own leaf says y 402 (and in the JP bundle the leaf says 401 against a parent 401). So a leaf's placement duplicates the parent's rather than being relative to it, and the two copies are not always byte-equal. The parent's is what compose honours; treat the leaf's as the source only for elements the parent does not declare — which is exactly the ring's case.

🟡 The ring carries 2 keyframes where the label carries 1, so it animates. What it does between them is not decoded here.

⚠️ screen render --focus is blind to the ring, and so was this page

el.focused is name-based on top-level elements, and a screen's buttons are .rat records whose focused twin is not itself a top-level element — so rendering build 5 with and without --focus produces an identical image. The reference renderer has the same blind spot the port reported, for the same reason: neither walks into the leaf. Fixing it is a renderer change, not a format question; the format is decoded above.

❔ Not established

  • ptbtn02b.t32 — a third variant, b, exists for button 02 only, same size as the base. Not seen on any capture. Not chased.
  • Whether a non-title archive uses the same two-element convention. Checked on GP_TITLE only.