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.
89 lines
3.2 KiB
Dart
89 lines
3.2 KiB
Dart
/// UI-04: hosts every visible HUD telemetry widget over a given area, and owns
|
|
/// entering/exiting edit mode -- the chrome (resize handles, the "Done" affordance)
|
|
/// only exists while editing, per the ticket's own reasoning against permanent-on-
|
|
/// screen edit chrome during a live ride.
|
|
///
|
|
/// Deliberately has no idea what a metric's live *value* is -- that's `RecordUiState`
|
|
/// (or equivalent), owned by whichever screen actually assembles a real HUD (UI-05).
|
|
/// [metricBuilder] is how that content gets in without this widget depending on it.
|
|
library;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../app/providers.dart';
|
|
import '../../hud/hud_metric.dart';
|
|
import 'draggable_resizable_hud_widget.dart';
|
|
|
|
class HudEditOverlay extends ConsumerStatefulWidget {
|
|
const HudEditOverlay({super.key, required this.metricBuilder});
|
|
|
|
final Widget Function(BuildContext context, HudMetric metric) metricBuilder;
|
|
|
|
@override
|
|
ConsumerState<HudEditOverlay> createState() => _HudEditOverlayState();
|
|
}
|
|
|
|
class _HudEditOverlayState extends ConsumerState<HudEditOverlay> {
|
|
bool _editing = false;
|
|
|
|
void _exitAndPersist() {
|
|
setState(() => _editing = false);
|
|
ref.read(hudLayoutControllerProvider.notifier).persist();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final layout = ref.watch(hudLayoutControllerProvider);
|
|
final notifier = ref.read(hudLayoutControllerProvider.notifier);
|
|
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final areaSize = Size(constraints.maxWidth, constraints.maxHeight);
|
|
return GestureDetector(
|
|
key: const Key('hud-edit-background'),
|
|
behavior: HitTestBehavior.translucent,
|
|
// Long-press empty HUD space to enter edit mode; while already editing, a
|
|
// plain tap on empty space exits it (and persists) -- both per the ticket's
|
|
// Design section.
|
|
onLongPress: _editing
|
|
? null
|
|
: () {
|
|
HapticFeedback.selectionClick();
|
|
setState(() => _editing = true);
|
|
},
|
|
onTap: _editing ? _exitAndPersist : null,
|
|
child: Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
for (final entry in layout.entries)
|
|
if (entry.value.visible)
|
|
DraggableResizableHudWidget(
|
|
key: ValueKey(entry.key),
|
|
layout: entry.value,
|
|
areaSize: areaSize,
|
|
editing: _editing,
|
|
onMoved: (col, row) => notifier.updatePosition(entry.key, col, row),
|
|
onResized: (colSpan, rowSpan) =>
|
|
notifier.updateSize(entry.key, colSpan, rowSpan),
|
|
child: widget.metricBuilder(context, entry.key),
|
|
),
|
|
if (_editing)
|
|
Positioned(
|
|
top: 8,
|
|
right: 8,
|
|
child: FilledButton(
|
|
key: const Key('hud-edit-done'),
|
|
onPressed: _exitAndPersist,
|
|
child: const Text('Done'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|