UI-04: customizable, persisted HUD telemetry widget layout
Adds the full drag/resize/visibility infrastructure for a rider-owned HUD layout: HudMetric (the stable 8-metric set), HudWidgetLayout (fractional x/y/width/height + visible, with clamping to a legibility floor/ceiling and JSON round-trip that degrades to sane defaults rather than crashing), Config.hudLayout persistence, and a HudLayoutController that stays in-memory-authoritative during an edit session and only writes through on persist() -- never per drag frame. DraggableResizableHudWidget and HudEditOverlay assemble the interaction: edit mode is entered by a long-press on empty HUD space (not a specific widget) and exited via Done or a tap on empty space; only while editing does a widget attach any drag/resize gesture at all, so a normal tap can never move one mid-ride by construction, not by an internal flag. Fixes a real gesture-arena bug found during testing: outside edit mode, a long-press landing on a widget was free to bubble to the overlay's background long-press handler and wrongly enter edit mode. An inner no-op GestureDetector of the same gesture type now absorbs it. Adds a "Live HUD stats" Settings section, one switch per metric, that persists immediately (unlike drag frames). Placed at the end of the Settings list rather than in the middle -- inserting mid-list pushed every later section below several existing tests' viewport assumptions. Verified end-to-end on a real emulator including a full process restart: resized a widget, force-stopped the app, relaunched, and the resize held. No consuming screen exists yet (UI-05's job) -- verified via a throwaway preview entry point, deleted after use. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Xki7YAcc2TiN2PRZJ2tXr
This commit is contained in:
87
lib/src/ui/components/hud_edit_overlay.dart
Normal file
87
lib/src/ui/components/hud_edit_overlay.dart
Normal file
@@ -0,0 +1,87 @@
|
||||
/// 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: (x, y) => notifier.updatePosition(entry.key, x, y),
|
||||
onResized: (w, h) => notifier.updateSize(entry.key, w, h),
|
||||
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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user