Files
rippr/docs/feedback/FB-08-hud-reflow-on-hide.md
uhryniuk 1a2e69cbf3 FB-08: reflow HUD row widgets when a metric is hidden or shown
setVisible now calls a new _reflowRow helper on the toggled metric's row,
spreading the remaining (or newly added) visible widgets evenly across the
4-column grid instead of leaving a blank cell where a hidden metric used to
be. Reflow is scoped to setVisible only; drag/resize behavior is unchanged.
2026-08-24 23:33:05 -05:00

158 lines
7.6 KiB
Markdown

# 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):
```dart
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`:
```dart
/// 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`):
```dart
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 `colSpan`s 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.