Files
rippr/docs/feedback/FB-02-hide-idle-speed-panel.md
uhryniuk 094e42a12f FB-02: hide the idle Speed panel until recording starts
Removes the idle GlassPanel/BigStat Speed-Status-Last ride block from
RecordScreen; the live map plus START control is now the entire idle
screen. HudEditOverlay is gated on !ui.isIdle. Migrates the five
affected widget tests to seed a recording trip and assert against the
HUD's own ValueKey(HudMetric.speed) widget instead of the old idle
Key('speed').

Also fixes a pre-existing gap this migration exposed: _HudMetricValue
had no mounted-mode awareness at all (no text scaling, and its
per-metric accent colours fall just under AA contrast against the
mounted theme's GlassPanel surface). Adds scale and mountedMode
params, reusing BigStat's own mechanisms (scale multiplier, explicit
onSurface ink) to fix both.
2026-08-24 15:53:14 -05:00

14 KiB

FB-02 — Hide the idle Speed panel until recording starts

Depends on — · Size S · Status Done

Goal

The Record screen's idle (not-recording) state currently shows a live "SPEED" readout plus status text before the rider has pressed Start at all. It should show nothing but the (live, per FB-01) map and the START control until a ride actually begins.

Context

Direct user feedback (docs/FEEDBACK.md, "Map Page" section):

Remove the "Speed" widget from the map screen when "start" hasn't been pressed yet. It blocks the entire screen, and you can't see the map at all. Speed and other widgets should only appear when recording starts.

Today's idle branch — lib/src/ui/record/record_screen.dart, inside build()'s Stack (~lines 196-223):

child: Stack(
  children: [
    if (ui.isIdle)
      Center(
        child: GlassPanel(
          padding: const EdgeInsets.all(24),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              // Current speed, not max: a max figure only moves when you beat
              // it, which reads as a frozen screen while riding steadily.
              // This is the 2.0.1 fix and it must not regress.
              BigStat(
                key: const Key('speed'),
                label: 'SPEED',
                value: speedValue,
                unit: speedUnit,
                scale: mountedMode ? mountedTextScale : 1.0,
              ),
              const SizedBox(height: 16),
              // A resting state, not a zeroed ride -- otherwise the screen
              // looks like a recording that is going nowhere.
              const StatRow(label: 'Status', value: 'Ready'),
              const StatRow(label: 'Last ride', value: 'See Rides'),
            ],
          ),
        ),
      )
    else
      Positioned.fill(
        bottom: _controlBarHeight(mountedMode),
        child: HudEditOverlay(
          metricBuilder: (context, metric) =>
              _HudMetricValue(metric: metric, ui: ui, units: units),
        ),
      ),
    ...

ui.isIdle is trip == null (RecordUiState.isIdle, ~line 42). The GlassPanel sizes to its content (mainAxisSize: MainAxisSize.min) so it isn't literally full-screen, but it's centered, opaque-ish (GlassPanel is a near-opaque translucent fill per its own doc comment elsewhere in this codebase), and shows a real live speed figure (_liveSpeed, updated every GPS tick via liveTelemetryProvider — see initState ~lines 86-88) before any ride exists — which is exactly what the feedback is describing as "blocking the screen" and confusing to see before pressing Start.

Design

Remove the entire if (ui.isIdle) ... else ... branching for HUD content — always render the HudEditOverlay branch, and have the idle state simply show nothing there (the HUD overlay area is empty when there's no trip, since there's nothing to edit/no metrics to place — confirm HudEditOverlay degrades gracefully with an idle RecordUiState, or gate it explicitly: only mount HudEditOverlay when !ui.isIdle, and render an empty SizedBox.shrink() (or nothing at all) for the idle case, so the full-bleed live map (per FB-01) is the entire idle screen behind the START control bar.

Concretely:

child: Stack(
  children: [
    if (!ui.isIdle)
      Positioned.fill(
        bottom: _controlBarHeight(mountedMode),
        child: HudEditOverlay(
          metricBuilder: (context, metric) =>
              _HudMetricValue(metric: metric, ui: ui, units: units),
        ),
      ),
    if (ui.isPaused)
      ...

(i.e. drop the idle Center(child: GlassPanel(...)) block entirely — no replacement content, not even a "Ready" label. The now-live background map, per FB-01, is the idle screen's entire content above the control bar.)

speedValue/speedUnit (formatSpeedParts(_liveSpeed, unit: units), ~line 176) become unused once the idle panel is removed — check whether _HudMetricValue's own HudMetric.speed case (formatSpeedParts(ui.currentSpeedKmh, unit: units).$1 at ~line 310) already computes this independently (it does — ui.currentSpeedKmh is _liveSpeed), so the final (speedValue, speedUnit) = ... line in build() should simply be deleted rather than left as dead code. Run flutter analyze to confirm no other lingering unused-variable warnings from this removal.

BigStat/StatRow imports in record_screen.dart (from ../components/stats.dart) may become unused if nothing else in this file still uses them — check before removing the import; confirmDialog (also from stats.dart) is used elsewhere in this file for the discard confirmation, so the import itself likely stays, just drop BigStat/ StatRow specifically if nothing else references them.

Implementation

  1. Remove the idle Center(child: GlassPanel(...)) block from record_screen.dart's build().
  2. Gate HudEditOverlay on !ui.isIdle (it was previously the else branch of the idle check — now it's the only branch, wrapped in its own if).
  3. Delete the now-unused final (speedValue, speedUnit) = formatSpeedParts(...) line.
  4. Run flutter analyze and remove any import that's now unused as a result.

Acceptance criteria

  • Idle state (no active trip) shows no Speed/Status/"Last ride" content at all — just the live background map and the control bar with only "START RECORDING" visible (unchanged from today).
  • The moment a trip starts recording, the full HUD (HudEditOverlay with the default-visible metrics: Speed, Avg Speed, Distance, Elapsed Time) appears exactly as it does today — this ticket only changes the idle state, not recording/paused.
  • flutter analyze clean (no unused-variable/import warnings from the removal).
  • flutter test green, test count only goes up (net effect after the test migrations below should still be positive or flat, never negative).

Tests

Several existing tests assert against the idle Speed panel specifically and must be migrated — not deleted — to seed an active recording and assert against the HUD's own Speed widget instead, since the underlying regressions they guard (black-on-black text, mounted-mode contrast/scale) are real risks that still apply once the same figure lives inside _HudMetricValue instead of the idle BigStat. The HUD's per-metric widgets are keyed ValueKey(entry.key) where entry.key is the HudMetric (lib/src/ui/components/hud_edit_overlay.dart ~line 63) — use find.byKey(const ValueKey(HudMetric.speed)) to locate the speed widget in a recording state, then descend into it the same way the old tests descended into Key('speed').

In test/widget_test.dart:

  • 'idle shows Ready and only START' (~line 126): remove the expect(find.text('Ready'), findsOneWidget); assertion (the "Ready"/"Status" text no longer exists at all); keep the START/PAUSE/STOP key assertions and the find.text('Distance') findsNothing check — both remain valid (this test's title may want updating to 'idle shows only START').
  • 'the headline is live speed, not max speed' (~line 138): change to seed a recording trip first (await repo.startTrip(1000); await pumpLive(tester, host(const RecordScreen(), map: false)); — see the identical pattern already used at ~line 205-206 in 'tapping PAUSE and then STOP...'), then assert find.text('SPEED') findsOneWidget / find.text('MAX SPEED') findsNothing exactly as before (Max Speed is not one of the four default-visible metrics — HudWidgetLayout.defaultFor's visible: index < columns with columns = 4 and HudMetric.values ordered speed, avgSpeed, distance, elapsedTime, maxSpeed, ... — so this assertion still holds true in the recording state).
  • 'text is legible against the dark ground' (~line 235): seed a recording trip the same way, then replace find.descendant(of: find.byKey(const Key('speed')), matching: find.text('0')) with find.descendant(of: find.byKey(const ValueKey(HudMetric.speed)), matching: find.text('0')). Same downstream assertions (colour != ripprBackground, colour != Colors.black) unchanged.
  • 'the mounted theme is high-contrast and text scales up (V3-05)' (~line 380) and 'the mounted-mode speed digit is legible against GlassPanel's own translucent surface...' (~line 400): both currently pump RecordScreen() idle with mountedMode: true and inspect Key('speed'). Migrate both to seed a recording trip first, then use ValueKey(HudMetric.speed) the same way. Confirm _HudMetricValue's value Text actually honors mountedMode's scale/theme the same way the old idle BigStat did (check _HudMetricValue.build() — if it currently has no mounted-mode awareness at all, that's a pre-existing gap this migration would newly expose, not something to paper over: if the mounted-mode font-size/contrast assertions fail against the HUD widget, that's a real, separate bug worth calling out plainly in this ticket's Outcome section rather than weakening the assertion to make it pass.
  • 'HUD widgets render over the map, not replacing it (UI-05)' (~line 219) already seeds a recording trip and asserts find.text('SPEED')/find.text('DISTANCE') — no change needed, but worth a quick sanity check that it still passes unmodified.

Risks

  • The mounted-mode migration above may surface that _HudMetricValue never actually had mounted-mode-aware styling (it was only ever exercised via the idle BigStat before now) — if so, this ticket's Outcome section must say so explicitly rather than silently deleting or weakening the assertion. Fixing that gap (if real) is reasonable to fold into this ticket since it's directly caused by this change, but keep the fix minimal (reuse whatever scale/theme mechanism BigStat already used) rather than redesigning _HudMetricValue.

Out of scope

The HUD grid/drag-resize/text-auto-fit rework (FB-03) — this ticket only removes idle content, it does not touch how the recording-state HUD widgets are laid out or sized. The live-map-while-idle behavior itself (FB-01) — this ticket assumes it lands separately; if FB-01 hasn't merged yet, the idle screen will just show today's (0,0)/zoom-2 map, which is still a strict improvement over a blocking Speed panel.

Outcome

Implemented exactly as designed. record_screen.dart's idle Center(child: GlassPanel(...)) block (Speed/Status/Last ride) is gone; HudEditOverlay is now gated on !ui.isIdle and is the Stack's only HUD-content branch. The dead final (speedValue, speedUnit) = ... line was deleted; BigStat/StatRow are no longer referenced in the file (the stats.dart import stays, since confirmDialog from the same file is still used for the discard dialog).

All five named tests were migrated per the ticket's Tests section:

  • 'idle shows Ready and only START' → renamed to 'idle shows only START', the Ready assertion removed, START/PAUSE/STOP and find.text('Distance') checks kept.
  • 'the headline is live speed, not max speed' → now seeds a recording trip and asserts against the HUD, same SPEED/MAX SPEED expectations.
  • 'text is legible against the dark ground' → now seeds a recording trip and descends into find.byKey(const ValueKey(HudMetric.speed)) instead of Key('speed').
  • The two mounted-mode tests (V3-05 scale/contrast, and UI-05 GlassPanel contrast) were migrated to seed a recording trip and use ValueKey(HudMetric.speed). The V3-05 test's unrelated Key('start') height check was swapped for Key('pause'), since a recording state has no START button. The GlassPanel-lookup test switched from find.byType(GlassPanel).first (an ancestor of Key('speed') in the old layout) to find.descendant(of: find.byKey(const ValueKey(HudMetric.speed)), matching: find.byType(GlassPanel)), since in the new HUD tree the per-metric GlassPanel is a descendant of the keyed widget (DraggableResizableHudWidget), not an ancestor.
  • 'HUD widgets render over the map, not replacing it (UI-05)' needed no change and still passes.

The risk the ticket flagged was real, in two parts, both now fixed:

  1. _HudMetricValue had no scale parameter at all — its label/value fontSizes were hardcoded (10/18), so the old BigStat-based mounted-mode text-scale assertion (64 * mountedTextScale) had no equivalent to migrate to. Added a scale field to _HudMetricValue, threaded from record_screen.dart's existing mountedMode local the same way BigStat was driven (mountedMode ? mountedTextScale : 1.0), and multiplied both font sizes by it. The migrated test now asserts 18 * mountedTextScale.
  2. Once (1) was fixed, the second mounted-mode test (GlassPanel contrast, UI-05) still failed: _HudMetricValue._valueColor's per-metric accent colours (colors.primary for Speed, tuned for the dark HUD mockup) measure 4.43:1 against the mounted theme's GlassPanel surface — just under the 4.5:1 AA threshold the test enforces. This is a real, separate, pre-existing gap this migration exposed: _HudMetricValue never accounted for mounted mode's own colour scheme in _valueColor at all (unlike BigStat, which always named its headline colour explicitly as colors.onSurface rather than trusting an accent). Fixed minimally by adding a mountedMode bool to _HudMetricValue that, when true, forces _valueColor to colors.onSurface for every metric — reusing BigStat's own explicit-ink fallback — while leaving the dark-theme mockup's per-metric accent colours (Speed: primary, Distance: secondary) untouched for the normal (non-mounted) case. This is a colour-only override scoped to mounted mode; no other _HudMetricValue behavior changed.

flutter analyze: clean (same 4 pre-existing info-level issues as baseline, none new). flutter test: 374 passing (unchanged from baseline — no tests were added or removed, five were migrated in place per the ticket).