Page template renderer: HERO1, HERO2, QUAD, GRID6 #59

Closed
opened 2026-08-31 17:55:57 +02:00 by robert · 3 comments
robert commented 2026-08-31 17:55:57 +02:00 (Migrated from git.butzei.de)

Goal

A generic renderer that draws any page descriptor. Replaces the hand-built ride layout originally planned in #9.

Acceptance criteria

  • 20 px status strip on every page: clock, Bluetooth, GPS quality, speed source, ride state - each a distinct glyph or shape, never colour alone
  • HERO1: one field, full 200x208, ~140 px digits
  • HERO2: hero 200x118 plus two cells 100x90; 96 px and 44 px digits
  • QUAD: 2x2 of 100x104, 56 px digits
  • GRID6: 2x3 of 100x69, 40 px digits
  • Custom numeric font subset to digits, ., :, -, / - system fonts stop around 42-49 px and cannot serve HERO1 or HERO2
  • Resource size of the custom font measured and recorded
  • Units rendered in the label, never beside the value
  • Labels checked for overflow in both German and English
  • Degrades without clipping on basalt - GRID6 may fall back to QUAD
  • Render cost measured; must hold 1 Hz updates comfortably

Files

  • watchapp/src/c/page_render.c
  • watchapp/resources/fonts/

Notes

See docs/DESIGN.md section 3.

Update — 2026-09-01: drawn digits, an emphasised slot, and a narrower Phase 1

Three changes.

1 — Digits are drawn, not shipped as fonts (D33). "A custom numeric font", singular, understated
the cost: Pebble bitmap fonts are generated per size, and this design needs 140, 96, 56, 44 and 40
px on emery plus round variants — about eight font resources. Since the glyph set is a dozen
characters, draw them procedurally instead.

  • Digits 0–9, ., :, -, / drawn procedurally from rounded bars
  • Continuous sizing — any pixel height, on any of the three geometries, without a new resource
  • Zero font resources for numerals; labels keep the system fonts
  • Render cost measured; must hold 1 Hz comfortably
  • Custom numeric font subset and resource size measured — no longer applicable

2 — QUAD and GRID6 get an emphasised slot (D29). As written, both present only equal-weight
numbers, which contradicts NFR-U7 — and two of the three shipped pages are QUAD.

  • Slot 1 of QUAD and GRID6 drawn larger than the rest: one emphasised cell plus three, and
    one plus five
  • Implemented as a size lookup keyed on slot index, not a separate layout path

3 — Phase 1 narrows to emery, HERO2 and QUAD (D32). HERO1 and GRID6 move to Phase 1b
alongside #51 and #62, so the first rideable build lands sooner.

  • Geometry functions take bounds in and give rects out, with no SDK calls, so #68 can test
    them on the host — this is what makes the deferred round work safe to do later

At the corrected ~200 DPI (see the D24 correction), GRID6's 40 px digits are 5.0 mm tall. It ships,
but it is not a default and should not be recommended in the configuration UI.

Update — 2026-09-02: render the two states differently

  • Greyed (stale) and -- (unavailable) are visually distinct, not the same treatment (D44)
  • Per rule 2 in docs/DESIGN.md, neither state is signalled by colour alone (#56)
## Goal A generic renderer that draws any page descriptor. Replaces the hand-built ride layout originally planned in #9. ## Acceptance criteria - [ ] 20 px status strip on every page: clock, Bluetooth, GPS quality, speed source, ride state - each a distinct glyph or shape, never colour alone - [ ] `HERO1`: one field, full 200x208, ~140 px digits - [ ] `HERO2`: hero 200x118 plus two cells 100x90; 96 px and 44 px digits - [ ] `QUAD`: 2x2 of 100x104, 56 px digits - [ ] `GRID6`: 2x3 of 100x69, 40 px digits - [ ] Custom numeric font subset to digits, `.`, `:`, `-`, `/` - system fonts stop around 42-49 px and cannot serve HERO1 or HERO2 - [ ] Resource size of the custom font measured and recorded - [ ] Units rendered in the label, never beside the value - [ ] Labels checked for overflow in both German and English - [ ] Degrades without clipping on `basalt` - `GRID6` may fall back to `QUAD` - [ ] Render cost measured; must hold 1 Hz updates comfortably ## Files - `watchapp/src/c/page_render.c` - `watchapp/resources/fonts/` ## Notes See docs/DESIGN.md section 3. ## Update — 2026-09-01: drawn digits, an emphasised slot, and a narrower Phase 1 **Three changes.** **1 — Digits are drawn, not shipped as fonts (D33).** "A custom numeric font", singular, understated the cost: Pebble bitmap fonts are generated **per size**, and this design needs 140, 96, 56, 44 and 40 px on `emery` plus round variants — about eight font resources. Since the glyph set is a dozen characters, draw them procedurally instead. - [ ] Digits `0`–`9`, `.`, `:`, `-`, `/` drawn procedurally from rounded bars - [ ] Continuous sizing — any pixel height, on any of the three geometries, without a new resource - [ ] **Zero font resources for numerals**; labels keep the system fonts - [ ] Render cost measured; must hold 1 Hz comfortably - [ ] ~~Custom numeric font subset~~ and ~~resource size measured~~ — no longer applicable **2 — `QUAD` and `GRID6` get an emphasised slot (D29).** As written, both present only equal-weight numbers, which contradicts NFR-U7 — and two of the three shipped pages are `QUAD`. - [ ] **Slot 1 of `QUAD` and `GRID6` drawn larger than the rest**: one emphasised cell plus three, and one plus five - [ ] Implemented as a size lookup keyed on slot index, not a separate layout path **3 — Phase 1 narrows to `emery`, `HERO2` and `QUAD` (D32).** `HERO1` and `GRID6` move to Phase 1b alongside #51 and #62, so the first rideable build lands sooner. - [ ] Geometry functions take bounds in and give rects out, with **no SDK calls**, so #68 can test them on the host — this is what makes the deferred round work safe to do later At the corrected ~200 DPI (see the D24 correction), `GRID6`'s 40 px digits are 5.0 mm tall. It ships, but it is not a default and should not be recommended in the configuration UI. ## Update — 2026-09-02: render the two states differently - [ ] Greyed (stale) and `--` (unavailable) are visually distinct, not the same treatment (D44) - [ ] Per rule 2 in `docs/DESIGN.md`, neither state is signalled by colour alone (#56)
Owner

PR #87 (merged): Phase 1 scope only, per this issue's own 2026-09-01 update (D32) \u2014 emery, HERO2 and QUAD. Delivers: procedurally-drawn digits (D33, seven-segment/rounded-bar, zero font resources, verified correct segment masks and a real '.' vs ':' distinction), the D29 emphasised-slot size lookup, host-testable bounds-in/rects-out geometry (28 new test cases), D44's stale-vs-unavailable visual distinction (hollow+strike vs solid dashes, confirmed in emulator screenshots), and a real measured render cost (24\u201337ms/frame in the emery emulator, comfortably inside the 1Hz budget). Caught and fixed one real bug from the screenshots themselves (GPS-quality icon bars rendering as a solid blob).\n\nLeft open \u2014 not this PR's job, per D32's own phasing: HERO1 and GRID6 templates (Phase 1b, alongside #51/#62), gabbro/basalt rendering (builds clean, geometry stays bounds-in/rects-out so this stays possible, but nothing has been visually verified on those two \u2014 that's #62's job), German labels (#51). Status strip's GPS-quality and speed-source icons are placeholders \u2014 no GPS or BLE sensor code exists anywhere yet to source real data from; clock and ride-state/Bluetooth are real. No periodic UI-repaint tick exists either, so the clock is correct per-render but doesn't advance while the screen sits idle \u2014 flagged as a gap for a future issue, not silently accepted.

PR #87 (merged): Phase 1 scope only, per this issue's own 2026-09-01 update (D32) \u2014 **emery, HERO2 and QUAD**. Delivers: procedurally-drawn digits (D33, seven-segment/rounded-bar, zero font resources, verified correct segment masks and a real '.' vs ':' distinction), the D29 emphasised-slot size lookup, host-testable bounds-in/rects-out geometry (28 new test cases), D44's stale-vs-unavailable visual distinction (hollow+strike vs solid dashes, confirmed in emulator screenshots), and a real measured render cost (24\u201337ms/frame in the emery emulator, comfortably inside the 1Hz budget). Caught and fixed one real bug from the screenshots themselves (GPS-quality icon bars rendering as a solid blob).\n\n**Left open** \u2014 not this PR's job, per D32's own phasing: `HERO1` and `GRID6` templates (Phase 1b, alongside #51/#62), `gabbro`/`basalt` rendering (builds clean, geometry stays bounds-in/rects-out so this stays possible, but nothing has been visually verified on those two \u2014 that's #62's job), German labels (#51). Status strip's GPS-quality and speed-source icons are placeholders \u2014 no GPS or BLE sensor code exists anywhere yet to source real data from; clock and ride-state/Bluetooth are real. No periodic UI-repaint tick exists either, so the clock is correct per-render but doesn't advance while the screen sits idle \u2014 flagged as a gap for a future issue, not silently accepted.
Owner

PR #120 merged — the Phase 1b remainder: HERO1 (full content area, 140px per DESIGN.md's literal figure) and GRID6 (2x3, 40px ordinary / 46px emphasised slot 0 per D29), plus page_render_resolve_template() implementing "GRID6 may fall back to QUAD on basalt" as a pixel comparison against DESIGN.md's own 100x69 reference cell (deliberately platform-blind, so it correctly does NOT special-case gabbro — round geometry stays #62's job). Reuses D44's stale-vs-unavailable distinction and #87's ellipsis overflow handling with zero new code — prv_draw_cell() doesn't know which template called it. Real verification: pebble build clean on all three targets, 38/38 host tests (hand-compiled with gcc, no cmake in this sandbox), real emulator screenshots on emery/gabbro/basalt, render cost measured (HERO1 25-46ms, GRID6 61-69ms on emery, up to 360ms on basalt — all comfortably under the 1Hz budget).

This issue stays open. Its own "degrades without clipping on basalt" acceptance criterion is not actually met yet: PR #120's own basalt verification — the first time anyone actually looked at basalt output for these templates — found that HERO1 clips on both emery and basalt at the literal 140px figure, and that pre-existing QUAD (unchanged since Phase 1/PR #87) also clips on basalt, meaning GRID6's fallback target isn't itself basalt-safe. Independently re-verified this by direct arithmetic against the shipped glyph-width-ratio constants before accepting the claim (a "28.4"-shaped value needs 2.3 x digit_height px of run width; 2.3x140=322px against emery's 200px/basalt's 144px content — confirms the clip without needing to trust the screenshot description alone).

Both findings share one root cause: page_render_digit_height(template, slot) has no notion of the cell's actual width, so nothing currently prevents a digit run from overflowing regardless of template. This is a mechanism change touching every template already shipped, correctly out of scope for a two-template Phase 1b PR — filed separately as #121 with the exact numbers, and #59 now depends on it.

44 issues closed (unrelated running count) — this one stays open pending #121.

PR #120 merged — the Phase 1b remainder: `HERO1` (full content area, 140px per DESIGN.md's literal figure) and `GRID6` (2x3, 40px ordinary / 46px emphasised slot 0 per D29), plus `page_render_resolve_template()` implementing "GRID6 may fall back to QUAD on basalt" as a pixel comparison against DESIGN.md's own 100x69 reference cell (deliberately platform-blind, so it correctly does NOT special-case gabbro — round geometry stays #62's job). Reuses D44's stale-vs-unavailable distinction and #87's ellipsis overflow handling with zero new code — `prv_draw_cell()` doesn't know which template called it. Real verification: `pebble build` clean on all three targets, 38/38 host tests (hand-compiled with gcc, no cmake in this sandbox), real emulator screenshots on emery/gabbro/basalt, render cost measured (HERO1 25-46ms, GRID6 61-69ms on emery, up to 360ms on basalt — all comfortably under the 1Hz budget). **This issue stays open.** Its own "degrades without clipping on basalt" acceptance criterion is not actually met yet: PR #120's own basalt verification — the first time anyone actually looked at basalt output for these templates — found that HERO1 clips on both emery and basalt at the literal 140px figure, **and** that pre-existing QUAD (unchanged since Phase 1/PR #87) also clips on basalt, meaning GRID6's fallback target isn't itself basalt-safe. Independently re-verified this by direct arithmetic against the shipped glyph-width-ratio constants before accepting the claim (a "28.4"-shaped value needs `2.3 x digit_height` px of run width; 2.3x140=322px against emery's 200px/basalt's 144px content — confirms the clip without needing to trust the screenshot description alone). Both findings share one root cause: `page_render_digit_height(template, slot)` has no notion of the cell's actual width, so nothing currently prevents a digit run from overflowing regardless of template. This is a mechanism change touching every template already shipped, correctly out of scope for a two-template Phase 1b PR — filed separately as **#121** with the exact numbers, and #59 now depends on it. 44 issues closed (unrelated running count) — this one stays open pending #121.
Owner

Re-checked every acceptance criterion honestly now that #121 (PR #123) has landed, and this issue is genuinely done:

  • Status strip, HERO2/QUAD (Phase 1, PR #87), HERO1/GRID6 (Phase 1b, PR #120) — all built.
  • Drawn digits, not fonts (D33) — the update's own text crosses off the custom-font-subset/resource-size criteria as "no longer applicable", superseded by this decision.
  • Emphasised slot (D29) — QUAD and GRID6 slot 0 both confirmed larger than their other slots, real host tests.
  • Units in the label, never beside the value — confirmed directly in page_render.c: field_unit() is appended into the label text (prv_format_label), never a separate value-adjacent element.
  • Label overflow — GTextOverflowModeTrailingEllipsis is applied to every label draw call, universally across all templates (not a per-template special case), so this was never just a GRID6-specific check; PR #120 additionally verified it with a real worst-case German label.
  • Stale vs. unavailable rendered differently (D44) — hollow+strike vs. solid dashes, done in Phase 1.
  • Render cost measured for every template on every platform (PR #87, #120) — all comfortably under the 1Hz budget.
  • Degrades without clipping on basalt — the one criterion that genuinely wasn't met until just now. PR #120 first surfaced (not introduced) that both HERO1 and pre-existing QUAD clipped on basalt; #121/PR #123 fixed the actual mechanism (page_render_fit_digit_height()), verified with 47 real host tests plus a clean pebble build on all three targets.

Not required by this issue's own literal criteria, correctly left to #62: gabbro's round-bezel overshoot (a page_render_content_rect() limitation affecting every template on round, not something #59 ever asked to be clipping-free against — this issue only names basalt). #121 stays open tracking that remainder without a formal dependency on #62 (would have created a cycle, since #62 itself already depends on #59 existing first — removed both problematic edges).

45 issues closed.

Re-checked every acceptance criterion honestly now that #121 (PR #123) has landed, and this issue is genuinely done: - Status strip, HERO2/QUAD (Phase 1, PR #87), HERO1/GRID6 (Phase 1b, PR #120) — all built. - Drawn digits, not fonts (D33) — the update's own text crosses off the custom-font-subset/resource-size criteria as "no longer applicable", superseded by this decision. - Emphasised slot (D29) — QUAD and GRID6 slot 0 both confirmed larger than their other slots, real host tests. - Units in the label, never beside the value — confirmed directly in `page_render.c`: `field_unit()` is appended into the label text (`prv_format_label`), never a separate value-adjacent element. - Label overflow — `GTextOverflowModeTrailingEllipsis` is applied to every label draw call, universally across all templates (not a per-template special case), so this was never just a GRID6-specific check; PR #120 additionally verified it with a real worst-case German label. - Stale vs. unavailable rendered differently (D44) — hollow+strike vs. solid dashes, done in Phase 1. - Render cost measured for every template on every platform (PR #87, #120) — all comfortably under the 1Hz budget. - **Degrades without clipping on basalt** — the one criterion that genuinely wasn't met until just now. PR #120 first surfaced (not introduced) that both HERO1 and pre-existing QUAD clipped on basalt; #121/PR #123 fixed the actual mechanism (`page_render_fit_digit_height()`), verified with 47 real host tests plus a clean `pebble build` on all three targets. **Not required by this issue's own literal criteria, correctly left to #62**: gabbro's round-bezel overshoot (a `page_render_content_rect()` limitation affecting every template on round, not something #59 ever asked to be clipping-free against — this issue only names basalt). #121 stays open tracking that remainder without a formal dependency on #62 (would have created a cycle, since #62 itself already depends on #59 existing first — removed both problematic edges). 45 issues closed.
Sign in to join this conversation.
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
robert/PedalPebble#59
No description provided.