Files
rippr/docs/feedback/FB-08-hud-reflow-on-hide.md

8.0 KiB

FB-08 — HUD widgets reflow to fill the gap when one is hidden

Depends on — · Size M · Status Done

Goal

Hiding a HUD metric must resize the remaining widgets in its row to fill the empty space. Today hiding a metric leaves an empty gap in the grid.

Context

Direct user feedback (docs/FEEDBACK.md):

When disabling some statistic during the HUD sessions, they should resize themselves, instead of leaving a gap by default.

lib/src/hud/hud_layout_controller.dart, setVisible (full method):

void setVisible(HudMetric metric, bool visible) {
  var current = state[metric] ?? HudWidgetLayout.defaultFor(metric);
  if (visible && _overlapsAnyOther(metric, current.copyWith(visible: true))) {
    current = HudWidgetLayout.nextFreeSlot(
      metric,
      occupied: state,
      colSpan: current.colSpan,
      rowSpan: current.rowSpan,
    );
  }
  state = {...state, metric: current.copyWith(visible: visible)};
}

This method only ever changes the toggled metric's own entry. No other metric's col, row, or colSpan ever changes as a side effect. When a metric in a row of four is hidden, the other three keep their exact original col/colSpan — the grid cell the hidden metric used to occupy simply renders nothing, showing as blank space in the HUD.

HudWidgetLayout (lib/src/hud/hud_widget_layout.dart) already has everything this ticket needs to build a row-reflow function: col, row, colSpan, rowSpan (all int), copyWith, and hudRectsOverlap. hudGridColumns is 4.

Design

  • Add a private helper to HudLayoutController:
    /// Spreads every visible widget in [row] evenly across the full grid width,
    /// left to right in their existing column order. A widget whose row has no other
    /// visible member is left alone -- there is nothing to redistribute.
    Map<HudMetric, HudWidgetLayout> _reflowRow(
      Map<HudMetric, HudWidgetLayout> layouts,
      int row,
    ) {
      final members = layouts.values.where((l) => l.visible && l.row == row).toList()
        ..sort((a, b) => a.col.compareTo(b.col));
      if (members.length < 2) return layouts;
      final baseSpan = hudGridColumns ~/ members.length;
      final extra = hudGridColumns % members.length;
      final result = Map<HudMetric, HudWidgetLayout>.from(layouts);
      var col = 0;
      for (var i = 0; i < members.length; i++) {
        final span = baseSpan + (i < extra ? 1 : 0);
        result[members[i].metric] = members[i].copyWith(col: col, colSpan: span);
        col += span;
      }
      return result;
    }
    
    A row of 4 becomes 4 equal columns (unchanged from today). A row of 3 becomes columns of width [2, 1, 1] (4 ~/ 3 = 1, remainder 1 goes to the first member). A row of 2 becomes [2, 2]. A row of 1 becomes [4] — a single remaining widget fills the whole row.
  • Call _reflowRow from setVisible, after computing the toggled metric's own new entry, using that metric's row before the toggle when hiding, and its row after placement when showing (the row a newly-shown metric lands in via defaultFor/nextFreeSlot):
    void setVisible(HudMetric metric, bool visible) {
      var current = state[metric] ?? HudWidgetLayout.defaultFor(metric);
      if (visible && _overlapsAnyOther(metric, current.copyWith(visible: true))) {
        current = HudWidgetLayout.nextFreeSlot(
          metric,
          occupied: state,
          colSpan: current.colSpan,
          rowSpan: current.rowSpan,
        );
      }
      final updated = {...state, metric: current.copyWith(visible: visible)};
      state = _reflowRow(updated, current.row);
    }
    
  • Reflow only touches the toggled metric's own row. A different row's widgets never move when a metric in another row is hidden or shown.
  • Reflow applies to every visible widget currently in that row, whether it is at its default position or one the rider dragged there manually. This ticket does not track "was this widget moved by the rider" — the row is always kept evenly filled, by design, since the feedback asks for the row to "resize themselves" whenever a metric in it is hidden, not only in the untouched-default case.
  • A drag or a resize (as opposed to a visibility toggle) does not trigger reflow — updatePosition/updateSize are unchanged. Reflow is scoped to setVisible only.

Implementation

  1. Add _reflowRow to HudLayoutController.
  2. Call _reflowRow at the end of setVisible, passing the toggled metric's row.
  3. Run flutter analyze and flutter test.

Acceptance criteria

  • Hiding one metric out of four in the same row resizes the remaining three to fill the row evenly — no empty gap.
  • Hiding a metric that is alone in its row leaves every other row untouched.
  • Showing a hidden metric back reflows its landing row to make room for it, shrinking the row's other members evenly.
  • A metric in a different row from the one just toggled never moves.
  • flutter analyze clean, flutter test green, test count only goes up.

Tests

In test/hud_layout_controller_test.dart (alongside the existing setVisible tests):

  • Hiding one of four same-row, equally-sized metrics leaves the remaining three each spanning hudGridColumns ~/ 3 or one more (matching the baseSpan/extra split), covering the full row width with no gap (assert the sum of colSpan across the remaining visible row members equals hudGridColumns).
  • Hiding a metric whose row has no other visible member leaves every other metric's col/row/colSpan exactly unchanged (assert full equality against the pre-toggle state for every other metric).
  • Showing a previously-hidden metric back into a row with existing members reflows that row to fit it — assert the sum of colSpan across the row (including the newly-shown metric) still equals hudGridColumns.
  • Toggling a metric in one row leaves a different row's own members' col/colSpan unchanged.

Risks

None significant — this only changes col/colSpan values within a single row at the moment of a visibility toggle; it does not touch persistence format, the drag/resize collision logic, or any other file.

Out of scope

FB-09's title-driven default sizing — this ticket does not change what colSpan a metric starts at, only how a row's members redistribute space among themselves when one of them is hidden or shown. Sequence FB-09 after this ticket merges, since both touch HudLayoutController/HudWidgetLayout and a large simultaneous diff in both is harder to review and merge than one after the other.

Outcome

Implemented _reflowRow in lib/src/hud/hud_layout_controller.dart exactly as specified in the Design section, and wired it into setVisible using the toggled metric's current.row (its row before the toggle when hiding, its row after defaultFor/nextFreeSlot placement when showing) — no deviation from the design.

Added four tests to test/hud_layout_controller_test.dart covering the four acceptance-criteria scenarios: hiding one of four same-row metrics reflows the remaining three to fill the row with no gap; hiding a metric alone in its row leaves every other metric's col/row/colSpan untouched; showing a previously-hidden metric into a row with an existing member reflows that row so their colSpans sum to hudGridColumns; and toggling a metric in one row leaves a different row's members' col/colSpan unchanged.

flutter analyze stayed clean (only pre-existing, unrelated info-level lint notices in lib/src/crash/crash_reporter.dart and lib/src/tiles/map_connectivity.dart, neither touched by this ticket). flutter test is green with a final count of 416 tests (412 baseline + 4 new), no regressions.

On-device verification, later pass (emulator available)

Confirmed live on a running recording: with all four default metrics visible (Speed / Average speed / Distance / Elapsed time), turning "Average speed" off in Settings' Live HUD Stats section immediately reflowed Speed and Distance to each take half the row, with no gap left behind — matching the acceptance criteria exactly, seen with a real before/after screenshot.