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,6 +1,9 @@
|
||||
/// UI-04: one telemetry widget on the customizable HUD -- positioned/sized from a
|
||||
/// [HudWidgetLayout]'s fractions against whatever pixel area it's given, draggable and
|
||||
/// resizable only while [editing] is true.
|
||||
/// UI-04/FB-03: one telemetry widget on the customizable HUD -- positioned/sized from a
|
||||
/// [HudWidgetLayout]'s grid cell against whatever pixel area it's given, draggable and
|
||||
/// resizable only while [editing] is true. Drag/resize snap to whole grid cells on
|
||||
/// gesture end (see [_DraggableResizableHudWidgetState]'s local pixel-offset fields);
|
||||
/// during the gesture itself the widget tracks the finger/handle continuously in
|
||||
/// pixels for a smooth feel.
|
||||
///
|
||||
/// Deliberately does not attach any drag gesture at all when [editing] is false --
|
||||
/// "outside edit mode, a normal tap never moves a widget" is guaranteed structurally
|
||||
@@ -36,11 +39,13 @@ class DraggableResizableHudWidget extends StatefulWidget {
|
||||
final bool editing;
|
||||
final Widget child;
|
||||
|
||||
/// Fractional x/y, already relative to [areaSize] -- not yet clamped; the caller
|
||||
/// (`HudLayoutController.updatePosition`) owns clamping so there is exactly one
|
||||
/// place that logic lives.
|
||||
final void Function(double x, double y) onMoved;
|
||||
final void Function(double width, double height) onResized;
|
||||
/// Final snapped grid coordinates -- called exactly once, at gesture end, not on
|
||||
/// every frame of movement (a local, uncommitted pixel offset tracks the live drag;
|
||||
/// see [_DraggableResizableHudWidgetState]). The caller
|
||||
/// (`HudLayoutController.updatePosition`) owns collision/bounds checking so there is
|
||||
/// exactly one place that logic lives.
|
||||
final void Function(int col, int row) onMoved;
|
||||
final void Function(int colSpan, int rowSpan) onResized;
|
||||
|
||||
@override
|
||||
State<DraggableResizableHudWidget> createState() =>
|
||||
@@ -50,13 +55,27 @@ class DraggableResizableHudWidget extends StatefulWidget {
|
||||
class _DraggableResizableHudWidgetState extends State<DraggableResizableHudWidget> {
|
||||
bool _grabbed = false;
|
||||
|
||||
/// Pixels, uncommitted, live only during a move gesture -- painted on top of the
|
||||
/// last-committed grid position so the drag tracks the finger smoothly, then reset
|
||||
/// to zero the instant the gesture ends (whether or not the parent accepted the
|
||||
/// resulting snapped position), which is what makes the widget visually "snap back"
|
||||
/// on a rejected move with no separate accept/reject callback needed.
|
||||
Offset _dragOffset = Offset.zero;
|
||||
|
||||
/// Pixels, uncommitted, live only during a resize gesture -- same idea as
|
||||
/// [_dragOffset].
|
||||
Size _resizeDelta = Size.zero;
|
||||
|
||||
double get _cellWidth => widget.areaSize.width / hudGridColumns;
|
||||
double get _cellHeight => widget.areaSize.height / hudGridRows;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final layout = widget.layout;
|
||||
final left = layout.x * widget.areaSize.width;
|
||||
final top = layout.y * widget.areaSize.height;
|
||||
final width = layout.width * widget.areaSize.width;
|
||||
final height = layout.height * widget.areaSize.height;
|
||||
final left = layout.col * _cellWidth + _dragOffset.dx;
|
||||
final top = layout.row * _cellHeight + _dragOffset.dy;
|
||||
final width = layout.colSpan * _cellWidth + _resizeDelta.width;
|
||||
final height = layout.rowSpan * _cellHeight + _resizeDelta.height;
|
||||
|
||||
Widget card = AnimatedScale(
|
||||
scale: _grabbed ? 1.05 : 1.0,
|
||||
@@ -71,13 +90,23 @@ class _DraggableResizableHudWidgetState extends State<DraggableResizableHudWidge
|
||||
setState(() => _grabbed = true);
|
||||
},
|
||||
onLongPressMoveUpdate: (details) {
|
||||
widget.onMoved(
|
||||
layout.x + details.offsetFromOrigin.dx / widget.areaSize.width,
|
||||
layout.y + details.offsetFromOrigin.dy / widget.areaSize.height,
|
||||
);
|
||||
setState(() => _dragOffset = details.offsetFromOrigin);
|
||||
},
|
||||
onLongPressEnd: (_) => setState(() => _grabbed = false),
|
||||
onLongPressCancel: () => setState(() => _grabbed = false),
|
||||
onLongPressEnd: (_) {
|
||||
final snappedCol =
|
||||
((layout.col * _cellWidth + _dragOffset.dx) / _cellWidth).round();
|
||||
final snappedRow =
|
||||
((layout.row * _cellHeight + _dragOffset.dy) / _cellHeight).round();
|
||||
widget.onMoved(snappedCol, snappedRow);
|
||||
setState(() {
|
||||
_grabbed = false;
|
||||
_dragOffset = Offset.zero;
|
||||
});
|
||||
},
|
||||
onLongPressCancel: () => setState(() {
|
||||
_grabbed = false;
|
||||
_dragOffset = Offset.zero;
|
||||
}),
|
||||
child: card,
|
||||
);
|
||||
} else {
|
||||
@@ -109,10 +138,23 @@ class _DraggableResizableHudWidgetState extends State<DraggableResizableHudWidge
|
||||
bottom: -8,
|
||||
child: GestureDetector(
|
||||
onPanUpdate: (details) {
|
||||
widget.onResized(
|
||||
layout.width + details.delta.dx / widget.areaSize.width,
|
||||
layout.height + details.delta.dy / widget.areaSize.height,
|
||||
);
|
||||
setState(() {
|
||||
_resizeDelta = Size(
|
||||
_resizeDelta.width + details.delta.dx,
|
||||
_resizeDelta.height + details.delta.dy,
|
||||
);
|
||||
});
|
||||
},
|
||||
onPanEnd: (_) {
|
||||
final snappedColSpan =
|
||||
((layout.colSpan * _cellWidth + _resizeDelta.width) / _cellWidth)
|
||||
.round();
|
||||
final snappedRowSpan =
|
||||
((layout.rowSpan * _cellHeight + _resizeDelta.height) /
|
||||
_cellHeight)
|
||||
.round();
|
||||
widget.onResized(snappedColSpan, snappedRowSpan);
|
||||
setState(() => _resizeDelta = Size.zero);
|
||||
},
|
||||
child: Container(
|
||||
key: const Key('hud-resize-handle'),
|
||||
|
||||
@@ -64,8 +64,9 @@ class _HudEditOverlayState extends ConsumerState<HudEditOverlay> {
|
||||
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),
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user