FB-03: grid-snapped HUD widgets with auto-fit, centered text
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.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
/// UI-04: the in-memory, authoritative HUD layout during an editing session.
|
||||
/// 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
|
||||
@@ -26,24 +26,47 @@ class HudLayoutController extends StateNotifier<Map<HudMetric, HudWidgetLayout>>
|
||||
|
||||
final Config? _config;
|
||||
|
||||
void updatePosition(HudMetric metric, double x, double y) {
|
||||
/// "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;
|
||||
state = {...state, metric: current.copyWith(x: x, y: y).clamped()};
|
||||
final candidate = current.copyWith(col: col, row: row).clampedToGrid();
|
||||
if (_overlapsAnyOther(metric, candidate)) return;
|
||||
state = {...state, metric: candidate};
|
||||
}
|
||||
|
||||
void updateSize(HudMetric metric, double width, double height) {
|
||||
void updateSize(HudMetric metric, int colSpan, int rowSpan) {
|
||||
final current = state[metric];
|
||||
if (current == null) return;
|
||||
state = {...state, metric: current.copyWith(width: width, height: height).clamped()};
|
||||
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 -- the ticket's own acceptance criterion. A metric
|
||||
/// that already has a real saved position keeps it.
|
||||
/// 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) {
|
||||
final current = state[metric] ?? HudWidgetLayout.defaultFor(metric);
|
||||
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)};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user