Files
rippr/docs/ui-redesign/UI-04-customizable-hud-widgets.md
uhryniuk 822003ed38 UI redesign: 8 tickets from the Stitch export analysis
Same shape as docs/v3/: Goal/Context/Design/Implementation/Acceptance/Tests/Risks/Out-of-scope, written before implementing.

UI-01: persistent 4-tab shell (StatefulShellRoute) replacing the current push/pop stack rooted at Record, with a single shared background map dimmed behind every tab and no header anywhere.
UI-02: animated skeleton map for the no-cache/no-network case, since the map is now a permanent background presence rather than opt-in per screen.
UI-03: shared GlassPanel/FloatingPill/PulsingLocationMarker component kit.
UI-04: draggable, resizable HUD telemetry widgets with a Settings visibility toggle -- split out from the screen redesign itself since it's a genuinely separate piece of engineering (a small widget-layout system, not a reskin).
UI-05: Map HUD/Record screen redesign -- the design north star every other screen ticket restyles toward.
UI-06: Plan & Route Planning -- correct in principle, explicitly restyled toward UI-05 rather than copying the Stitch export's drifted styling.
UI-07: Rides History -- live dimmed map background (not the mockup's static image), search/filter, real per-ride map thumbnails, and real distinct summary stats (fixing the export's 4-identical-cards generation artifact).
UI-08: theme migration to Modern Professional Dark, superseding V3-16's safety-orange direction.

README documents the dependency order, and two categories worth keeping separate: what was named explicitly by the person commissioning this (map-on-every-tab, no header, offline skeleton, customizable HUD) versus what the mockups themselves got inconsistent (nav icon count, duplicate summary cards, wrong active-tab highlight) and should not be copied as intentional.
2026-08-23 18:25:35 -05:00

115 lines
6.5 KiB
Markdown

# UI-04 — Customizable HUD telemetry widgets
**Depends on** UI-03 · **Size** L · **Status** Not started
## Goal
Let the rider drag each floating telemetry widget (Speed, Distance, etc.) to wherever
they want it on screen, resize it, and choose in Settings which metrics appear live at
all — a persisted, personal HUD layout rather than a fixed one.
## Context
The Stitch Map HUD mockup has a fixed horizontal row of four telemetry cards with one
interaction: tap a card to temporarily expand it, tap again (or tap another) to collapse
it back. That's a reasonable default, but by explicit instruction the actual goal is
further than that: **the rider owns the layout.** Hold-and-drag to reposition, resize by
dragging a handle, and a Settings screen listing every available metric with a toggle for
whether it's shown at all. This supersedes the mockup's tap-to-expand interaction rather
than coexisting with it — see Design.
## Design
**Data model.** A `HudWidgetLayout` per metric:
```
{ metricId, x, y, width, height, visible }
```
`x`/`y`/`width`/`height` stored as fractions of the available HUD area (0.0-1.0), not
absolute pixels, so a layout saved on one device/orientation still makes sense on
another. `metricId` is a stable enum (`speed`, `avgSpeed`, `distance`, `elapsedTime`,
`maxSpeed`, `elevationGain`, ...) — the same set `RecordUiState`/`Trip` already expose,
not a new data source.
**Available metrics vs. visible metrics.** Every metric the app already tracks is a
candidate; `visible` (toggled in Settings, see below) controls whether it currently has a
HUD widget at all. A metric toggled off entirely has no position/size to speak of until
turned back on, at which point it gets a sane default slot (not wherever it happened to
be last, unless a position was already saved).
**Interaction:**
- **Move:** long-press-and-drag anywhere on a widget's `GlassPanel` body. A brief haptic
and a subtle scale-up on press-start signal "this is now grabbable," matching the
weight of a real physical action rather than an accidental tap.
- **Resize:** a small drag handle in one corner, visible only while a widget is in "edit
mode" (see below), not in every-day use — a resize handle sitting on screen permanently
during a live ride is visual noise the rider doesn't need mid-ride.
- **Edit mode:** entered explicitly (e.g. a long-press anywhere on the HUD area that
isn't a specific widget, or a dedicated "Edit HUD" toggle from Settings/a long-press on
empty HUD space) rather than every telemetry widget always being draggable — an
always-draggable widget risks an accidental drag mid-ride when the rider meant to tap
it for something else. Exiting edit mode (tap "Done", or tap empty space) persists the
layout.
- **Constraints:** every widget clamps to stay fully within the safe HUD area on drag/
resize end — never under the bottom nav bar, never off-screen, never smaller than a
legibility floor (the number must stay readable) or larger than some sane ceiling.
**Settings integration.** A new "Live HUD stats" section: a checkbox/switch per available
metric, controlling `visible`. Order in the list is stable (doesn't reflect current HUD
position) so it's easy to scan.
**Persistence.** One `Config` field (JSON-encoded map of `metricId` → `HudWidgetLayout`),
same pattern as every other `Config` preference in this app — see `mountedMode`,
`crashReportingEnabled` for the shape to follow.
## Implementation
1. `HudWidgetLayout` model + JSON encode/decode, with defaults for every known metric
(a sensible starting grid, e.g. the Stitch mockup's horizontal row) so a fresh install
has a working, if plain, HUD before the rider customizes anything.
2. `Config.hudLayout` getter/setter, following the established `Config` pattern.
3. `HudLayoutController` (or a `Riverpod` `StateNotifier`) holding the current layout in
memory, seeded from `Config`, written back to `Config` on every edit-mode exit — not
on every drag frame, to avoid hammering `SharedPreferences` mid-drag.
4. `DraggableResizableHudWidget`: wraps a telemetry `GlassPanel`, handles the long-press-
to-grab gesture, the resize handle, and the clamp-on-release logic.
5. `HudEditOverlay`: the edit-mode chrome (resize handles, a "Done" affordance, maybe a
subtle grid/snap guide) shown only while editing.
6. Settings section: "Live HUD stats," one row per metric with a switch bound to
`visible`.
## Acceptance criteria
- [ ] A telemetry widget can be dragged to a new position and the new position survives
an app restart
- [ ] A telemetry widget can be resized within sane min/max bounds
- [ ] Dragging or resizing never leaves a widget partially off-screen or behind the nav
bar
- [ ] Toggling a metric off in Settings removes its HUD widget immediately; toggling it
back on restores it (at its last saved position if one exists, otherwise a default)
- [ ] Outside of edit mode, a normal tap on a telemetry widget does not move it
(no accidental drags during a ride)
- [ ] Layout is per-install (`Config`), not per-ride
## Tests
- Unit: `HudWidgetLayout` JSON round-trips exactly; unknown/missing metric ids on load
fall back to defaults rather than crashing
- Unit: clamp logic — a drag/resize ending outside allowed bounds is corrected to the
nearest valid position/size, exercised with fixed geometry inputs (no real gestures
needed to test the math)
- Widget: drag gesture moves a widget and the moved position is what gets persisted
(simulate via `TestGesture`, matching how other drag interactions in this codebase are
tested)
- Widget: toggling a metric's Settings switch adds/removes its HUD widget
- Widget: a tap outside edit mode does not trigger a move
## Risks
- **Scope creep toward a general-purpose layout editor.** Keep the interaction minimal:
drag, resize, toggle visibility. No z-ordering, no custom widget shapes, no per-metric
color customization — those are separate future tickets if wanted, not this one.
- Persisting on every drag frame would thrash `SharedPreferences`; persist only on
edit-mode exit, keep in-memory state authoritative during an active drag.
- This ticket removes the Stitch mockup's tap-to-expand interaction rather than layering
on top of it — a widget the rider has manually resized should not also silently resize
itself on tap, which would fight the rider's own choice.
## Out of scope
Z-ordering/overlap resolution between widgets (widgets simply clamp to stay on-screen;
overlapping each other is the rider's own choice to avoid). Per-metric colour
customization. Sharing/exporting a HUD layout between devices.