Page carousel navigation with long-press jumps (#60) #85

Merged
robert merged 1 commit from area/page-carousel into main 2026-09-04 09:48:04 +02:00
Owner

Closes #60.

Replaces page_view.c's issue #8 placeholder with the real button model from docs/DESIGN.md section 5, wired on top of #58's page descriptors (PR #81) and #11's ride state machine (PR #84).

What changed

  • Up/Down step to the previous/next visible page, wrapping at both ends. "Visible" skips a page with no live field (page_has_live_field(), #58's primitive built for exactly this) — unless none of the three default pages has data yet (cold boot, before the phone has sent anything), in which case every page counts as visible rather than leaving a blank ring. Only the landing page is ever rendered — a run of skipped pages never flickers past on screen.
  • Long Up jumps straight to page 1, deliberately bypassing the skip rule (an explicit rider request, not a ring step — matches DESIGN.md's "manual navigation always wins" precedent for Nav auto-switching).
  • Long Down is supposed to jump to the map. There is no map page or view anywhere in this codebase yet, so this is a documented no-op: an APP_LOG line and a comment at the call site, not a fake fallback to page 1 (which would make long Up and long Down indistinguishable) and not a silent, unexplained nothing.
  • Select starts/pauses (ride_link_start_pause()); long Select begins a stop confirmation (ride_link_stop_request()); Back leaves the app, confirmed first only if a ride is running or paused. Both confirmations share one on-screen prompt and the same Select/Back roles reversing between "confirm" and "cancel". The stop confirmation reads ride_state_stop_pending() directly as its single source of truth (per state.h's own documented expectation that a stray Select during confirmation must route to confirm/cancel, not resume); the exit confirmation is a local carousel.c flag, since state.h has no "leave the app" primitive and doesn't need one.
  • Checked against the current Core Devices SDK headers on disk (SDK 4.33.1, not assumed from older docs): window_single_click_subscribe()'s own doc says that when a long-click handler is also registered on a button, the single-click handler is held back until the system knows the press wasn't a long one — that's what gives "long-press doesn't also fire the short-press action" for free, no hand-rolled debounce needed. window_long_click_subscribe()'s doc says the back button cannot take a long-click handler at all ("a long press will always terminate the app"), which is why there's no long-Back handling here — and DESIGN.md's own button table has no long-Back row either, so this isn't a gap.
  • Current page persisted via a new PERSIST_KEY_ACTIVE_PAGE_INDEX (added to page.h's key registry, persist_read_int()/persist_write_int()), alongside #58's existing PERSIST_KEY_PAGE_DESCRIPTORS.

What's honestly still absent, and why

  • Navigation page insertion ("only when a route is loaded"): there is no route-loaded state anywhere in this codebase (that's Meridian/companion-side work, much later in the tracker). Treated as always-false — the Nav page never appears. Not faked with a placeholder flag.
  • Disabled pages: "disabled" has no concrete meaning yet (no settings surface, #61). Only the auto-hidden half of "disabled and auto-hidden pages are skipped" is implemented (page_has_live_field()); the carousel has nothing to source "disabled" from until #61 lands.
  • "Buttons stay responsive while map frames are arriving": there's no map-frame-arrival code in this codebase yet, so this criterion is currently vacuous — nothing to test it against. Not simulated with synthetic traffic.

Renamed page_view.c/.h to carousel.c/.h in place (git history preserved via git mv) rather than adding a new carousel.c alongside them. page_view.c's own header comment already said "issue #60 replaces this handler wholesale" — keeping both would just be two files owning one Window and one job. ride_link.h and state.h's comments describing page_view.c as the still-current placeholder are updated to point at carousel.c instead, so nothing in the tree keeps citing stale facts.

Verification

  • pebble build clean on emery, gabbro and basalt (all three targetPlatforms).
  • All 68 existing host tests (test_fields/test_page/test_state) still pass unchanged — page.h's enum addition doesn't touch anything they assert on.
  • Exercised live in the emery and gabbro emulators via pebble emu-button (click and long-click, both buttons) with pebble logs and pebble screenshot as ground truth:
    • Up/Down cycles Ride/Effort/Progress, wrapping both directions.
    • Long Up jumps to Ride from anywhere; long Down logs the documented no-op.
    • Select start -> pause -> resume, each queuing and sending the right CMD (ride_link.c's own logging).
    • Long Select shows "Stop ride? / Select=Y / Back=N"; Select confirms (ride stops, prompt clears); Back cancels (prompt clears, ride still running) — screenshotted both platforms, no clipping on gabbro's round display.
    • Back while running shows "Leave app? / Back=Y / Select=N"; Select cancels back to the page; Back again confirms and exits — the firmware's own exit log reported Still allocated <0B>, i.e. no leak on exit.
    • Back while stopped/idle exits immediately with no prompt.
    • heap_bytes_free() logged identical before/after on every single transition throughout the whole run (107512 -> 107512 the entire time on emery).
    • Reinstalling after exit reopened the app directly on the last-viewed page (Effort), confirming persistence survives a restart.

https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt

Closes #60. Replaces page_view.c's issue #8 placeholder with the real button model from docs/DESIGN.md section 5, wired on top of #58's page descriptors (PR #81) and #11's ride state machine (PR #84). ### What changed - **Up/Down** step to the previous/next *visible* page, wrapping at both ends. "Visible" skips a page with no live field (`page_has_live_field()`, #58's primitive built for exactly this) — unless *none* of the three default pages has data yet (cold boot, before the phone has sent anything), in which case every page counts as visible rather than leaving a blank ring. Only the landing page is ever rendered — a run of skipped pages never flickers past on screen. - **Long Up** jumps straight to page 1, deliberately bypassing the skip rule (an explicit rider request, not a ring step — matches DESIGN.md's "manual navigation always wins" precedent for Nav auto-switching). - **Long Down** is supposed to jump to the map. There is no map page or view anywhere in this codebase yet, so this is a **documented no-op**: an `APP_LOG` line and a comment at the call site, not a fake fallback to page 1 (which would make long Up and long Down indistinguishable) and not a silent, unexplained nothing. - **Select** starts/pauses (`ride_link_start_pause()`); **long Select** begins a stop confirmation (`ride_link_stop_request()`); **Back** leaves the app, confirmed first only if a ride is running or paused. Both confirmations share one on-screen prompt and the same Select/Back roles reversing between "confirm" and "cancel". The stop confirmation reads `ride_state_stop_pending()` directly as its single source of truth (per state.h's own documented expectation that a stray Select during confirmation must route to confirm/cancel, not resume); the exit confirmation is a local `carousel.c` flag, since state.h has no "leave the app" primitive and doesn't need one. - Checked against the current Core Devices SDK headers on disk (SDK 4.33.1, not assumed from older docs): `window_single_click_subscribe()`'s own doc says that when a long-click handler is also registered on a button, the single-click handler is held back until the system knows the press wasn't a long one — that's what gives "long-press doesn't also fire the short-press action" for free, no hand-rolled debounce needed. `window_long_click_subscribe()`'s doc says the back button cannot take a long-click handler at all ("a long press will always terminate the app"), which is why there's no long-Back handling here — and DESIGN.md's own button table has no long-Back row either, so this isn't a gap. - Current page persisted via a new `PERSIST_KEY_ACTIVE_PAGE_INDEX` (added to page.h's key registry, `persist_read_int()`/`persist_write_int()`), alongside #58's existing `PERSIST_KEY_PAGE_DESCRIPTORS`. ### What's honestly still absent, and why - **Navigation page insertion** ("only when a route is loaded"): there is no route-loaded state anywhere in this codebase (that's Meridian/companion-side work, much later in the tracker). Treated as always-false — the Nav page never appears. Not faked with a placeholder flag. - **Disabled pages**: "disabled" has no concrete meaning yet (no settings surface, #61). Only the auto-hidden half of "disabled and auto-hidden pages are skipped" is implemented (`page_has_live_field()`); the carousel has nothing to source "disabled" from until #61 lands. - **"Buttons stay responsive while map frames are arriving"**: there's no map-frame-arrival code in this codebase yet, so this criterion is currently vacuous — nothing to test it against. Not simulated with synthetic traffic. ### page_view.c vs. carousel.c Renamed page_view.c/.h to carousel.c/.h in place (git history preserved via `git mv`) rather than adding a new carousel.c alongside them. page_view.c's own header comment already said "issue #60 replaces this handler wholesale" — keeping both would just be two files owning one Window and one job. ride_link.h and state.h's comments describing page_view.c as the still-current placeholder are updated to point at carousel.c instead, so nothing in the tree keeps citing stale facts. ### Verification - `pebble build` clean on **emery**, **gabbro** and **basalt** (all three `targetPlatforms`). - All 68 existing host tests (test_fields/test_page/test_state) still pass unchanged — page.h's enum addition doesn't touch anything they assert on. - Exercised live in the **emery** and **gabbro** emulators via `pebble emu-button` (click and long-click, both buttons) with `pebble logs` and `pebble screenshot` as ground truth: - Up/Down cycles Ride/Effort/Progress, wrapping both directions. - Long Up jumps to Ride from anywhere; long Down logs the documented no-op. - Select start -> pause -> resume, each queuing and sending the right CMD (`ride_link.c`'s own logging). - Long Select shows "Stop ride? / Select=Y / Back=N"; Select confirms (ride stops, prompt clears); Back cancels (prompt clears, ride still running) — screenshotted both platforms, no clipping on gabbro's round display. - Back while running shows "Leave app? / Back=Y / Select=N"; Select cancels back to the page; Back again confirms and exits — the firmware's own exit log reported `Still allocated <0B>`, i.e. no leak on exit. - Back while stopped/idle exits immediately with no prompt. - `heap_bytes_free()` logged identical before/after on every single transition throughout the whole run (107512 -> 107512 the entire time on emery). - Reinstalling after exit reopened the app directly on the last-viewed page (Effort), confirming persistence survives a restart. https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt
Page carousel navigation with long-press jumps (#60)
Some checks failed
dev-artifact / build-pbw (push) Has been cancelled
dev-artifact / build-apk (push) Has been cancelled
fast-lane / host-c-tests (push) Has been cancelled
fast-lane / jvm-tests (push) Has been cancelled
fast-lane / pebble-build (push) Has been cancelled
fast-lane / lint-and-secrets (push) Has been cancelled
fast-lane / meta-declares-required-jobs (push) Has been cancelled
fast-lane / host-c-tests (pull_request) Has been cancelled
fast-lane / jvm-tests (pull_request) Has been cancelled
fast-lane / pebble-build (pull_request) Has been cancelled
fast-lane / lint-and-secrets (pull_request) Has been cancelled
fast-lane / meta-declares-required-jobs (pull_request) Has been cancelled
dev-artifact / publish (push) Has been cancelled
7b78029231
Replaces page_view.c's issue #8 placeholder with the real button model
from docs/DESIGN.md section 5, wired on top of #58's page descriptors
and #11's ride state machine:

- Up/Down step to the previous/next *visible* page, wrapping at both
  ends. "Visible" skips a page with no live field (page.h's
  page_has_live_field(), issue #58's primitive built for exactly this)
  unless literally none of the three default pages has data yet (cold
  boot), in which case every page counts as visible rather than
  leaving a blank ring. Only the landing page is ever rendered, so a
  run of skipped pages never flickers past on screen.
- Long Up jumps straight to page 1, bypassing the skip rule (an
  explicit request, not a ring step). Long Down is meant to jump to
  the map; there is no map page anywhere in this codebase yet, so this
  is a documented no-op (APP_LOG + a comment at the call site) rather
  than a fake destination or a silent do-nothing.
- Select starts/pauses (ride_link_start_pause()); long Select begins a
  stop confirmation (ride_link_stop_request()); Back leaves the app,
  confirmed first only if a ride is running or paused. Both
  confirmations share one on-screen prompt and the same Select=confirm
  / Back=cancel (or vice versa) shape. The stop confirmation's pending
  state is read directly from ride_state_stop_pending() (single source
  of truth, per state.h's own documented expectation); the exit
  confirmation is a local flag, since state.h has no primitive for
  "leave the app" and doesn't need one.
- Confirmed against the current Core Devices SDK (pebble.h, SDK
  4.33.1, read locally, not assumed from old docs):
  window_single_click_subscribe()'s own doc says a long-click handler
  on the same button defers the single-click handler until the press
  is known not to be long, which is what gives "long-press doesn't
  also fire the short-press action" for free. Back cannot take a
  long-click handler at all ("a long press will always terminate the
  app") - matches DESIGN.md's table, which has no long-Back row.
- Current page persisted via a new PERSIST_KEY_ACTIVE_PAGE_INDEX
  (page.h's registry), alongside #58's existing
  PERSIST_KEY_PAGE_DESCRIPTORS.
- Navigation page insertion ("only when a route is loaded") and
  disabled-page skipping are both honestly absent: there is no
  route-loaded state and no settings surface (#61) in this codebase
  yet, so implementing either now would mean inventing a fake flag.
  Documented at the point where each would plug in.
- page_view.c/.h are renamed to carousel.c/.h in place (git history
  preserved) rather than adding a new carousel.c alongside them —
  page_view.c's own header comment already said "#60 replaces this
  handler wholesale"; having both would just be two files owning one
  Window and one job. ride_link.h and state.h's comments describing
  page_view.c as the current placeholder are updated to point at
  carousel.c.

Verified: `pebble build` clean on emery, gabbro and basalt. All 68
existing host tests (test_fields/test_page/test_state) still pass
unchanged. Exercised live in the emery and gabbro emulators via
`pebble emu-button`: Up/Down cycling, long Up jump, long Down no-op,
Select start/pause/resume, long Select stop-confirm, Back
cancel-stop/exit-confirm/cancel-exit/confirm-exit, and Back exiting
immediately with no prompt once stopped — heap_bytes_free() flat
across every transition, and the firmware's own exit log confirmed
"Still allocated <0B>" on quit. Active-page persistence confirmed by
reinstalling and observing the app reopen on the last-viewed page.

Claude-Session: https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt
robert merged commit 5e29c151a0 into main 2026-09-04 09:48:04 +02:00
Sign in to join this conversation.
No description provided.