Files
rippr/lib/src/ui/components/hud_edit_overlay.dart
uhryniuk 2d118b46a2 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.
2026-08-24 15:53:00 -05:00

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'),
),
),
],
),
);
},
);
}
}