Skip to content

The black header band reaches the panel's top row - #34

Merged
ma-r-s merged 3 commits into
xteinkfrom
app/topband
Sep 4, 2026
Merged

The black header band reaches the panel's top row#34
ma-r-s merged 3 commits into
xteinkfrom
app/topband

Conversation

@ma-r-s

@ma-r-s ma-r-s commented Sep 4, 2026

Copy link
Copy Markdown
Owner

The owner, looking at his X4 Pro from below: a strip of white above the title on every apps and games screen, invisible head-on.

Measured on his device (framebuffer pulled over Developer Mode): rows 0-9 pure white, 10-23 solid black. Ten rows is exactly the bezel's top inset.

toybox::headerBand() TOOK the band rect from panel row 0 but PAINTED it from the safe rect, so the band was ten rows short -- not shifted. Title ink, band bottom and rule already sit where the design puts them on both the device capture and the simulator baseline, so nothing below moves. Paint and ink now follow different rules: paint spans the full panel from row 0, ink still centres between the safe top and the band's bottom.

41 call sites in 27 files -- every Toybox black band, not only the two screens reported.

Eleven screens had a worse variant the v1.5.3 bezel work missed: checkers, connect four, knucklebones, minesweeper, yahtzee and xkcd called screen.header(props) raw, so their band was inset on THREE sides -- white above and a white column down each edge.

A cold review found the same rule broken the other way: trivia's hand-drawn right label and chess's gear centred over the covered rows and rode ~5px high. Fixed.

The ui suite could not have caught any of it: its fake device has safeArea = {}, pinning the wrong geometry as correct by construction. A bezelDevice() now carries the measured {10,1,0,1}.

Left open deliberately: those eleven screens still lack absoluteChrome, so their band bottom sits 10px below their menus'. That moves content and wants all eleven rendered and judged, not smuggled into a paint fix. Recorded in docs/bezel-insets.md.

🤖 Generated with Claude Code

Mario, looking up at the device from below: "on the very, very top above
the titles of everything in apps and games, I see white space."

toybox::headerBand() filled from the bezel's safe top, so the panel's top
ten rows (nine in the sim) stayed paper under a black band. A covered row
is not an invisible row -- it is invisible HEAD-ON. The glass sits above
the panel, so an eye below the device sees past the bezel's edge and reads
a white strip. Every black header in the fork had one: 41 call sites in 27
files, the shelf and both its folders among them.

The band's paint now starts at the panel's physical top-left corner and
spans the full width; only the ink moves with the insets, centred between
the safe top and the band's bottom edge exactly as before. The bottom edge
is still taken from the content rect, so nothing below the band moves --
verified in the simulator by pixel-diffing the shelf's GAMES folder and
Connect Four's board before and after: the rows below the band are
byte-identical and the title's ink band does not shift.

Eleven screens (checkers, connect four, knucklebones, minesweeper and
yahtzee boards and results, plus xkcd's chrome()) called screen.header()
straight and were missed by the bezel flip in v1.5.3. Their band came off
the safe rect, so it was inset on THREE sides: a white strip above it and a
white column down each side. They go through headerBand() now, which is
one code path for every band in the fork and fixes their sides too. Their
layout does not move: header(props) and headerBand() take the same rect
from the same content top.

Still open, deliberately: those eleven lack absoluteChrome, so their band's
bottom edge sits 10px below their own menus'. That is a layout change --
content moves up by the top inset -- and belongs in a change that renders
and judges all eleven, not in a paint fix. Recorded in docs/bezel-insets.md.

Docs: docs/bezel-insets.md -- the claim "the band's covered rows stay
paper" was the bug, and the note that the 21-screen render sweep was
entry screens only (all menus, all headerBand) is why the boards were
missed by it as well.

Tests: three in host-tests/ui, all watched red first. The suite's device()
has safeArea = {}, which pins the same geometry with and without the glass
by construction and is why nothing here could see this; bezelDevice()
carries the measured {10,1,0,1}. Reverting the paint fix reddens all six
screens checked; reverting only the eleven call sites reddens exactly those
five; centring the ink over the covered rows too (the obvious wrong fix,
which passes every coverage check) reddens the ink-centring check.
A cold review of the paint fix swept for its twin and found the same rule
broken the other way round: paint that stopped at the safe top was the white
strip; ink that starts at the panel top is a label riding above the title it
sits beside.

- trivia's right label (QUESTION / ANSWER / the score) boxed itself over the
  whole 76px band and ink-centred in that, while headerBand() centres the
  title between the safe top and the band's bottom. About 5px apart on the
  device. Forehead already does this correctly; trivia was the last hand-drawn
  right label that did not.
- chess's gear used (kHeaderHeight - size) / 2. It is a decoration riding the
  band exactly like the shelf's folder mark, which uses toybox::bandCenterY.
  There is no Screen in ChessActivity, so it reads the same insets from
  getOrientedViewableTRBL and does the same arithmetic.

Also from the review, neither reachable today, both cheap:

- toybox::bandCenterY derived its band height from the kHeaderHeight constant
  rather than screen.theme().headerHeight. Identical for all four callers now;
  wrong the first time a screen raises its band, which two already do.
- headerBand()'s underlay fills square and borderless. Every band in the fork
  is (popup radius 0, headerUnderline 0), so it is correct, but the assumption
  was unwritten and a styled band would show square corners under a round one.

Tests: the trivia label check compares the two runs' INK centres, not their
rects -- they use different cuts on purpose, which is the whole reason the
label is hand-drawn. Watched red against the previous line (test_ui.cpp:7939,
title and label 5px apart) and green after. The band checks now also assert
the panel's side COLUMNS, not only its covered rows: a band taken off the safe
rect was inset on three edges and a top-only check called half of that fixed.

Docs: docs/bezel-insets.md, the decorations item, naming both misses.

Not covered by tests: the chess gear lives in an Activity, which the ui suite
cannot compile. Verified by render instead.
@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
crossplay Ready Ready Preview Sep 4, 2026 5:50am UTC

@ma-r-s
ma-r-s merged commit 4076d7d into xteink Sep 4, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant