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.
6.5 KiB
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
GlassPanelbody. 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
HudWidgetLayoutmodel + 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.Config.hudLayoutgetter/setter, following the establishedConfigpattern.HudLayoutController(or aRiverpodStateNotifier) holding the current layout in memory, seeded fromConfig, written back toConfigon every edit-mode exit — not on every drag frame, to avoid hammeringSharedPreferencesmid-drag.DraggableResizableHudWidget: wraps a telemetryGlassPanel, handles the long-press- to-grab gesture, the resize handle, and the clamp-on-release logic.HudEditOverlay: the edit-mode chrome (resize handles, a "Done" affordance, maybe a subtle grid/snap guide) shown only while editing.- 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:
HudWidgetLayoutJSON 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.