Page template renderer: HERO2 and QUAD, Phase 1 (#59) #87
No reviewers
Labels
No labels
area:companion
area:docs
area:shared
area:tooling
area:watchapp
blocker
kind:chore
kind:feature
kind:spike
kind:test
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
robert/PedalPebble!87
Loading…
Reference in a new issue
No description provided.
Delete branch "area/page-template-renderer"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes part of #59 — scoped per the issue's own 2026-09-01/09-02 updates (D32): emery only, HERO2 and QUAD only. HERO1 and GRID6 move to Phase 1b alongside #51/#62, per the issue text, not a cut made here.
What this ships
page_render_geometry.h/.c— pure geometry (bounds in, rects out, zero<pebble.h>, zero SDK calls anywhere in this file — the issue's own acceptance criterion, and D34). Status strip layout, HERO2/QUAD slot grids, label/value split within a cell, and D29's emphasised-slot size lookup keyed on slot index (page_render_digit_height(template, slot)) — a 2-line array-plus-case extension covers GRID6's emphasis in Phase 1b, no new layout path needed.Digits
0-9and.,:,-,/are drawn procedurally (D33) from a seven-segment / rounded-bar geometry, continuously sized to any pixel height — zero font resources for numerals. Chose the seven-segment approach over an outline/stroke-font approach specifically because it's the one a non-graphics reader can verify by eye against the segment table in the code, and it needs no rotation/trig math anywhere (documented in the header's top comment, including why/is a staircase of small squares rather than a rotated bar).Host-tested directly — no stub needed, same as
fields.c— bywatchapp/tests/test_page_render_geometry.c: 28 assertions covering slot rects for HERO2/QUAD at emery's real 200×228 dimensions (including exact tiling/no-clipping checks againstcontent— the same property that makes the deferred round variant safe later), the D29 emphasis lookup, glyph-metric proportions at HERO2/QUAD's actual digit heights (44/56/76/96px), run-centring math, and the seven-segment table itself (segment counts per digit, a couple of exact segment rects, punctuation shapes).page_render.h/.c— the SDK-touching draw entry point (GContextcalls), split out the same wayride_link.csits alongsidestate.c: not part of the host suite, needs the real SDK.page_render_draw(ctx, bounds, desc, now)draws the status strip then HERO2 or QUAD.Stale (
field_is_stale()) and unavailable (--) render visibly differently, never by colour alone (D44, and the issue's 2026-09-02 update / #56):graphics_draw_round_rect, stroke not fill) plus one strike bar across the whole value, in grey (PP_COLOR_STALE).--glyph pair, solid, in ink — no special-casing needed anywhere, becausefield_is_stale()is documented to always be false while a field is unavailable (fields.h), so--flows through the exact same glyph pipeline as any digit and just happens to be a dash shape.Labels use
GTextOverflowModeTrailingEllipsis(a real, verified SDK feature) for overflow handling rather than hand-tuned per-font abbreviations — this is the actual answer to the issue's "labels checked for overflow in English" criterion, and it's a mechanism, not a guess, so it won't need revisiting when #51 (German) lands.Status strip (20px, every page)
Clock and ride-state are real data:
FIELD_CLOCKis now sourced from the watch's own RTC for the first time anywhere in this codebase (grepped before writingpage_render.c— nothing else has ever set it), refreshed once per render call (this codebase has no periodic UI-repaint tick yet, so it's correct at every page transition/button press but won't itself advance mid-idle — a future redraw-tick issue gets that for free). Ride state and Bluetooth link both reusestate.h's existingride_state_local()/ride_state_connected()(#11) — Bluetooth link is real, not a placeholder, sinceconnection_serviceis already wired up.GPS quality and active speed source have no data source anywhere in this codebase — no GPS code, no BLE sensor code exists (checked, not assumed) — and each renders one fixed placeholder shape (three signal-strength bars for GPS; a hollow diamond for speed source), documented in the code and here, same honest-scoping precedent as #60's unbuilt Map page. All four icons plus the clock are distinct shapes per the no-colour-alone rule; colour (where used) only reinforces.
carousel.c
Adds a second
Layer(s_page_layer) alongside the existing confirmationTextLayer, toggled bylayer_set_hidden()rather than resizing one layer between two jobs. Nothing allocates per render —heap_bytes_free()stays flat across every page transition in the emulator log (confirmed below).Verification
Host tests: all 96 pass (28 new + 68 existing, zero regressions) —
fields,page,state, and the newpage_render_geometry, each compiled and run directly with a plain hostgcc -std=c11 -Wall -Wextra -Werror(nocmakebinary available in this environment, so I compiled eachtests/CMakeLists.txttarget's exact source/include list by hand — same flags, same sources).Builds:
pebble buildclean for all three platforms (gabbro,emery,basalt) — zero warnings from any new or changed file.gabbro/basaltbuild because the shared code (palette.h,fields.c,page.c, and this PR's own files) has no emery-specific assumptions baked in, but nobody has looked at what either of them draws — that's #62's job (round) and general basalt degradation, both Phase 1b/out of scope here, per D32.Emulator, live, emery — screenshots below. Since nothing decodes ride-metric AppMessage keys yet (confirmed in
main.c's own inbox-handler comment — that's separate, unbuilt scope), every field would show--with no way to see real digit rendering, so I temporarily seededfields.c's store frommain.c'sprv_init()(a_TEMPfunction, clearly marked, reverted before this commit —git diffonmain.cis empty in this PR) purely to drive the emulator for these screenshots.HERO2 (Ride page), normal: hero "SPEED km/h" at 96px reads cleanly as "25.2"; "AVG SPEED km/h" cell at 44px reads "21.6"; "DIST km" shows
--(never set) — solid dashes, clearly distinct from a greyed number.HERO2, stale: same three fields,
FIELD_GROUP_RIDEaged past its 6s threshold — hero and cell both render hollow with a strike bar through the middle, in grey. DIST stays--, unchanged and unaffected (D44: unavailable and stale are independent).QUAD (Effort page), normal: HEART (slot 0, emphasised) visibly and unmistakably larger than POWER/CADENCE (56px) — direct confirmation of D29. AVG PWR shows
--.QUAD, stale: HR/POWER/CADENCE all hollow+struck (shared
FIELD_GROUP_RIDE/own-group aging both expired by the time of this shot), AVG PWR still--.Also caught and fixed one real bug from the screenshots, not from reading the code: the GPS-quality icon's three "signal bars" were originally drawn hollow (
graphics_draw_rectstroke), but at the icon's actual ~4px bar width a 3px stroke covers almost the whole shape, so hollow and solid were visually indistinguishable. Switched to filled bars — documented inpage_render.c.Render cost — measured, not assumed
carousel.c'sprv_page_layer_update_proc()brackets everypage_render_draw()call withtime_ms()and logs the delta. From the emulator log across several page transitions:24-37ms per render, comfortably inside the 1Hz (1000ms) budget — about 2.5-3.7% of it. This is QEMU-emulator-measured, not on physical hardware; flagging that distinction rather than presenting it as an on-device number.
heap_bytes_free()is unchanged across every transition in the same log (103240 -> 103240) — no allocation anywhere in the render path, digit drawing included.Not in this PR
HERO1,GRID6— Phase 1b (D32), per the issue's own 2026-09-01 update.gabbro/basaltdegradation — builds clean, not rendered/verified. Geometry stays bounds-in/rects-out specifically so that door isn't closed.page_render.c, not this issue's job to add.https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt
Scoped per the issue's own 2026-09-01/09-02 updates (D32): emery only, HERO2 and QUAD only. HERO1 and GRID6 move to Phase 1b alongside #51/#62. - page_render_geometry.h/.c: pure geometry (bounds in, rects out, zero <pebble.h> or SDK calls anywhere) — status strip, HERO2/QUAD slot grids, label/value split, and D29's emphasised-slot size lookup keyed on slot index (a 2-line extension covers GRID6 in Phase 1b, no new layout path). Digits 0-9 and . : - / are drawn from a seven-segment / rounded-bar geometry, continuously sized to any pixel height (D33) — no font resources for numerals. Host-tested directly (no stub needed, same as fields.c) by tests/test_page_render_geometry.c: 28 assertions covering slot rects at emery's real 200x228, tiling/no-clipping checks, the D29 emphasis lookup, glyph metrics at HERO2/QUAD's actual digit heights, and the seven-segment table itself. - page_render.h/.c: the SDK-touching draw entry point (GContext calls), split out the same way ride_link.c sits alongside state.c. Stale (field_is_stale()) and unavailable ("--") render visibly differently and never by colour alone (D44, #56): stale draws the same rects hollow (graphics_draw_round_rect) plus one strike bar, in grey; unavailable renders as the ordinary "--" glyph pair, solid, in ink — no special casing needed, since field_is_stale() is already false while a field is unavailable. Labels use GTextOverflowModeTrailingEllipsis for graceful overflow rather than hand-tuned abbreviations. - Status strip (20px, every page): clock and ride-state glyph are real data (FIELD_CLOCK, now sourced from the watch's own RTC for the first time; ride_state_local()/ride_state_connected() from #11). GPS quality and active speed source have no data source anywhere in this codebase (no GPS code, no BLE sensor code) and render one fixed placeholder shape each, documented in code and here — same honest-scoping precedent as #60's unbuilt Map page. - carousel.c: adds a second Layer (s_page_layer) alongside the existing confirmation TextLayer, toggled by visibility rather than resizing one layer between two jobs. Nothing allocates per render — heap_bytes_free() stays flat across every page transition in the emulator log. Verified live in the emery emulator (screenshots in the PR description): both templates, normal/stale/unavailable all distinguishable, D29 emphasis visibly larger, real ticking clock, status icons all distinct shapes. gabbro and basalt still build clean (shared code, no PBL_IF_ROUND_ELSE needed yet — nothing here is emery-specific). Render cost measured in the emulator: 24-37ms per page_render_draw() call, comfortably inside the 1Hz budget. Claude-Session: https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPtScreenshots referenced above, from the live emery emulator:
HERO2 (Ride), normal — hero "25.2" at 96px, cell "21.6" at 44px, DIST

--:HERO2, stale — same fields, hollow + strike-through, grey; DIST still

--and unaffected:QUAD (Effort), normal — HEART (emphasised, D29) visibly larger than POWER/CADENCE; AVG PWR

--:QUAD, stale — HR/POWER/CADENCE hollow + struck, AVG PWR still

--; note the status strip's fixed GPS-bars/speed-source placeholders and the real ticking clock/Bluetooth dot/ride-state ring across all four: