diff --git a/docs/feedback/FB-08-hud-reflow-on-hide.md b/docs/feedback/FB-08-hud-reflow-on-hide.md index 1e8bfaa..e62dd73 100644 --- a/docs/feedback/FB-08-hud-reflow-on-hide.md +++ b/docs/feedback/FB-08-hud-reflow-on-hide.md @@ -1,6 +1,6 @@ # FB-08 — HUD widgets reflow to fill the gap when one is hidden -**Depends on** — · **Size** M · **Status** Not started +**Depends on** — · **Size** M · **Status** Done ## Goal Hiding a HUD metric must resize the remaining widgets in its row to fill the empty @@ -136,3 +136,22 @@ metric starts at, only how a row's members redistribute space among themselves w 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. diff --git a/lib/src/hud/hud_layout_controller.dart b/lib/src/hud/hud_layout_controller.dart index 187e849..bb93283 100644 --- a/lib/src/hud/hud_layout_controller.dart +++ b/lib/src/hud/hud_layout_controller.dart @@ -67,7 +67,30 @@ class HudLayoutController extends StateNotifier> rowSpan: current.rowSpan, ); } - state = {...state, metric: current.copyWith(visible: visible)}; + final updated = {...state, metric: current.copyWith(visible: visible)}; + state = _reflowRow(updated, current.row); + } + + /// 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 _reflowRow( + Map 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.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; } Future persist() async => _config?.setHudLayout(state); diff --git a/test/hud_layout_controller_test.dart b/test/hud_layout_controller_test.dart index 453839f..afb7bd8 100644 --- a/test/hud_layout_controller_test.dart +++ b/test/hud_layout_controller_test.dart @@ -110,6 +110,80 @@ void main() { } }); + test('hiding one of four same-row metrics reflows the remaining three to fill ' + 'the row with no gap', () async { + final controller = HudLayoutController(await freshConfig()); + + controller.setVisible(HudMetric.avgSpeed, false); + + final remaining = [HudMetric.speed, HudMetric.distance, HudMetric.elapsedTime] + .map((m) => controller.state[m]!) + .toList() + ..sort((a, b) => a.col.compareTo(b.col)); + final totalSpan = remaining.fold(0, (sum, l) => sum + l.colSpan); + expect(totalSpan, hudGridColumns); + // baseSpan = 4 ~/ 3 = 1, extra = 1 -- the first (leftmost) member absorbs it. + expect(remaining[0].colSpan, 2); + expect(remaining[1].colSpan, 1); + expect(remaining[2].colSpan, 1); + expect(controller.state[HudMetric.avgSpeed]!.visible, isFalse); + }); + + test('hiding a metric whose row has no other visible member leaves every ' + 'other metric untouched', () async { + final controller = HudLayoutController(await freshConfig()); + // Row 4 is unused by any default layout -- move speed there alone. + controller.updatePosition(HudMetric.speed, 0, 4); + + final others = { + for (final m in HudMetric.values.where((m) => m != HudMetric.speed)) + m: controller.state[m]!, + }; + + controller.setVisible(HudMetric.speed, false); + + for (final entry in others.entries) { + final after = controller.state[entry.key]!; + expect(after.col, entry.value.col, reason: '${entry.key} col changed'); + expect(after.row, entry.value.row, reason: '${entry.key} row changed'); + expect(after.colSpan, entry.value.colSpan, + reason: '${entry.key} colSpan changed'); + } + }); + + test('showing a previously-hidden metric back into a row with existing ' + 'members reflows that row to fit it', () async { + final controller = HudLayoutController(await freshConfig()); + + // maxSpeed and movingTime both default to row 2, hidden, at adjacent columns. + controller.setVisible(HudMetric.maxSpeed, true); + controller.setVisible(HudMetric.movingTime, true); + + final maxSpeed = controller.state[HudMetric.maxSpeed]!; + final movingTime = controller.state[HudMetric.movingTime]!; + expect(maxSpeed.row, movingTime.row, + reason: 'test setup assumes both land in the same row'); + expect(maxSpeed.colSpan + movingTime.colSpan, hudGridColumns); + }); + + test('toggling a metric in one row leaves a different row\'s members ' + 'untouched', () async { + final controller = HudLayoutController(await freshConfig()); + final row2Before = { + for (final m in [HudMetric.maxSpeed, HudMetric.movingTime, HudMetric.elevationGain]) + m: controller.state[m]!, + }; + + controller.setVisible(HudMetric.avgSpeed, false); + + for (final entry in row2Before.entries) { + final after = controller.state[entry.key]!; + expect(after.col, entry.value.col, reason: '${entry.key} col changed'); + expect(after.colSpan, entry.value.colSpan, + reason: '${entry.key} colSpan changed'); + } + }); + test('persist writes the current state to Config', () async { final config = await freshConfig(); final controller = HudLayoutController(config);