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.
115 lines
6.5 KiB
Markdown
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.
|