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

152 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ✅ 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:
```text
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:
```text
+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`](../captures/ui-layout/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`](../captures/title-builds/live-main-menu-options-focused.png)
against
[`live-main-menu.png`](../captures/title-builds/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](ui-render-tone-curve.md)) 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`](../../../crates/sylpheed-formats/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.