Replace free-form fractional HUD widget positioning with a 4x8 int grid (col/row/colSpan/rowSpan), reject drag/resize moves that would overlap another visible widget, and re-place a re-enabled metric into the next free grid slot instead of stacking it on an occupier. Drag/resize now track a local pixel offset during the gesture and only snap/commit once at gesture end. HUD widget text is wrapped in FittedBox(scaleDown) so it never overflows and stays centered at any grid size.
75 lines
3.4 KiB
Dart
75 lines
3.4 KiB
Dart
/// UI-04/FB-03: the in-memory, authoritative HUD layout during an editing session.
|
|
///
|
|
/// Seeded from `Config` once, and written back to it only on [persist] -- not on every
|
|
/// drag/resize frame, which would hammer `SharedPreferences` mid-drag (the ticket's own
|
|
/// named risk). A drag in progress only ever touches this in-memory state; [persist] is
|
|
/// what a `HudEditOverlay` calls when the rider exits edit mode.
|
|
library;
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../config/config.dart';
|
|
import 'hud_metric.dart';
|
|
import 'hud_widget_layout.dart';
|
|
|
|
class HudLayoutController extends StateNotifier<Map<HudMetric, HudWidgetLayout>> {
|
|
// `Config?`, not `Config` -- at app boot `configProvider` is briefly null (loaded
|
|
// asynchronously post-first-frame, same as every other Config-seeded provider in
|
|
// this app). Defaults stand in until it resolves; [persist] silently no-ops rather
|
|
// than blocking, since editing the HUD before Config has loaded isn't a real path a
|
|
// rider can reach in practice.
|
|
HudLayoutController(this._config)
|
|
: super(
|
|
_config?.hudLayout ??
|
|
{for (final m in HudMetric.values) m: HudWidgetLayout.defaultFor(m)},
|
|
);
|
|
|
|
final Config? _config;
|
|
|
|
/// "Reject and no-op" is the collision rule for drag/resize -- simplest correct
|
|
/// behaviour, and it's equivalent to "revert to last valid" here since `state` is
|
|
/// never mutated until the candidate passes the check. A drag/resize that would
|
|
/// overlap another visible widget simply has no effect for that gesture; the widget
|
|
/// stays at its last valid position.
|
|
void updatePosition(HudMetric metric, int col, int row) {
|
|
final current = state[metric];
|
|
if (current == null) return;
|
|
final candidate = current.copyWith(col: col, row: row).clampedToGrid();
|
|
if (_overlapsAnyOther(metric, candidate)) return;
|
|
state = {...state, metric: candidate};
|
|
}
|
|
|
|
void updateSize(HudMetric metric, int colSpan, int rowSpan) {
|
|
final current = state[metric];
|
|
if (current == null) return;
|
|
final candidate = current.copyWith(colSpan: colSpan, rowSpan: rowSpan).clampedToGrid();
|
|
if (_overlapsAnyOther(metric, candidate)) return;
|
|
state = {...state, metric: candidate};
|
|
}
|
|
|
|
bool _overlapsAnyOther(HudMetric metric, HudWidgetLayout candidate) => state.values
|
|
.where((l) => l.visible && l.metric != metric)
|
|
.any((other) => hudRectsOverlap(candidate, other));
|
|
|
|
/// A metric turned on for the first time (no meaningfully-placed prior layout) gets
|
|
/// [HudWidgetLayout.defaultFor] rather than whatever stale position it held from
|
|
/// before it was last turned off. A metric that already has a real saved position
|
|
/// keeps it -- unless that slot is now occupied by another visible widget (the
|
|
/// "goes crazy" bug this ticket exists to fix), in which case it's placed in the
|
|
/// next free grid cell instead of popping up on top of the occupier.
|
|
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)};
|
|
}
|
|
|
|
Future<void> persist() async => _config?.setHudLayout(state);
|
|
}
|