Merge FB-03: grid-snapped HUD widgets with auto-fit text
# Conflicts: # docs/feedback/FB-03-grid-snapped-hud-widgets.md # lib/src/ui/record/record_screen.dart
This commit is contained in:
@@ -96,10 +96,10 @@ void main() {
|
||||
...config.hudLayout,
|
||||
HudMetric.speed: const HudWidgetLayout(
|
||||
metric: HudMetric.speed,
|
||||
x: 0.33,
|
||||
y: 0.44,
|
||||
width: 0.25,
|
||||
height: 0.1,
|
||||
col: 2,
|
||||
row: 3,
|
||||
colSpan: 2,
|
||||
rowSpan: 1,
|
||||
visible: true,
|
||||
),
|
||||
};
|
||||
@@ -107,12 +107,27 @@ void main() {
|
||||
await config.setHudLayout(edited);
|
||||
final reloaded = config.hudLayout;
|
||||
|
||||
expect(reloaded[HudMetric.speed]!.x, 0.33);
|
||||
expect(reloaded[HudMetric.speed]!.y, 0.44);
|
||||
expect(reloaded[HudMetric.speed]!.col, 2);
|
||||
expect(reloaded[HudMetric.speed]!.row, 3);
|
||||
});
|
||||
|
||||
test('a metric missing from a saved layout (e.g. one added in a later app '
|
||||
'version) falls back to its default rather than being omitted', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'hud_layout': '{"speed": {"col": 1, "row": 1, "colSpan": 1, "rowSpan": 2, '
|
||||
'"visible": true}}',
|
||||
});
|
||||
final config = await Config.load();
|
||||
final layout = config.hudLayout;
|
||||
|
||||
expect(layout.keys.toSet(), HudMetric.values.toSet());
|
||||
final expectedDefault = HudWidgetLayout.defaultFor(HudMetric.distance);
|
||||
expect(layout[HudMetric.distance]!.col, expectedDefault.col);
|
||||
expect(layout[HudMetric.distance]!.visible, expectedDefault.visible);
|
||||
});
|
||||
|
||||
test('an old-shaped (pre-grid) saved layout falls back to defaults rather than '
|
||||
'crashing', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'hud_layout': '{"speed": {"x": 0.1, "y": 0.1, "width": 0.2, "height": 0.1, '
|
||||
'"visible": true}}',
|
||||
@@ -121,9 +136,8 @@ void main() {
|
||||
final layout = config.hudLayout;
|
||||
|
||||
expect(layout.keys.toSet(), HudMetric.values.toSet());
|
||||
final expectedDefault = HudWidgetLayout.defaultFor(HudMetric.distance);
|
||||
expect(layout[HudMetric.distance]!.x, expectedDefault.x);
|
||||
expect(layout[HudMetric.distance]!.visible, expectedDefault.visible);
|
||||
final expectedDefault = HudWidgetLayout.defaultFor(HudMetric.speed);
|
||||
expect(layout[HudMetric.speed]!.col, expectedDefault.col);
|
||||
});
|
||||
|
||||
test('corrupted stored JSON falls back to defaults rather than crashing',
|
||||
|
||||
@@ -5,9 +5,26 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:rippr/src/app/providers.dart';
|
||||
import 'package:rippr/src/config/config.dart';
|
||||
import 'package:rippr/src/hud/hud_metric.dart';
|
||||
import 'package:rippr/src/hud/hud_widget_layout.dart';
|
||||
import 'package:rippr/src/ui/components/draggable_resizable_hud_widget.dart';
|
||||
import 'package:rippr/src/ui/components/hud_edit_overlay.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// A representative HUD child -- structurally the same FittedBox(label + value)
|
||||
/// shape as `record_screen.dart`'s private `_HudMetricValue`, which can't be
|
||||
/// referenced directly from outside its library.
|
||||
Widget _representativeHudChild() => const FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('SPEED', style: TextStyle(fontSize: 10, letterSpacing: 1)),
|
||||
SizedBox(height: 4),
|
||||
Text('12.3 km/h', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
/// UI-04: drag/toggle behaviour of the customizable HUD, simulated via `TestGesture`
|
||||
/// the same way other drag interactions in this codebase are tested (see
|
||||
/// `route_planner_screen_test.dart`'s waypoint-drag coverage).
|
||||
@@ -71,7 +88,9 @@ void main() {
|
||||
final speedFinder = find.text('Speed');
|
||||
final gesture = await tester.startGesture(tester.getCenter(speedFinder));
|
||||
await tester.pump(kLongPressTimeout + kPressTimeout);
|
||||
await gesture.moveBy(const Offset(40, 60));
|
||||
// Large enough to guarantee at least one grid cell of movement in both axes
|
||||
// regardless of the test surface's exact pixel size.
|
||||
await gesture.moveBy(const Offset(300, 300));
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
@@ -80,8 +99,8 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final after = config.hudLayout[HudMetric.speed]!;
|
||||
expect(after.x, isNot(before.x));
|
||||
expect(after.y, isNot(before.y));
|
||||
expect(after.col, isNot(before.col));
|
||||
expect(after.row, isNot(before.row));
|
||||
});
|
||||
|
||||
testWidgets('a drag while not editing does not move the widget', (tester) async {
|
||||
@@ -99,8 +118,8 @@ void main() {
|
||||
|
||||
expect(find.byKey(const Key('hud-edit-done')), findsNothing);
|
||||
final after = config.hudLayout[HudMetric.speed]!;
|
||||
expect(after.x, before.x);
|
||||
expect(after.y, before.y);
|
||||
expect(after.col, before.col);
|
||||
expect(after.row, before.row);
|
||||
});
|
||||
|
||||
testWidgets('only visible metrics render a widget', (tester) async {
|
||||
@@ -110,4 +129,138 @@ void main() {
|
||||
expect(find.text('Speed'), findsOneWidget, reason: 'visible by default');
|
||||
expect(find.text('Max speed'), findsNothing, reason: 'hidden by default');
|
||||
});
|
||||
|
||||
testWidgets('a drag ending over a cell occupied by another visible widget leaves '
|
||||
'the dragged widget\'s rendered position unchanged', (tester) async {
|
||||
await tester.pumpWidget(host());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.longPress(find.byKey(const Key('hud-edit-background')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final speedFinder = find.byKey(const ValueKey(HudMetric.speed));
|
||||
final before = tester.getTopLeft(speedFinder);
|
||||
|
||||
// Speed (col 0, row 0) dragged exactly one cell right lands squarely on
|
||||
// avgSpeed's (col 1, row 0) cell -- both share the same row-0 default rowSpan, so
|
||||
// this is a genuine, not merely edge-touching, overlap.
|
||||
final gesture = await tester.startGesture(tester.getCenter(find.text('Speed')));
|
||||
await tester.pump(kLongPressTimeout + kPressTimeout);
|
||||
await gesture.moveBy(Offset(tester.view.physicalSize.width / tester.view.devicePixelRatio / hudGridColumns, 0));
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(const Key('hud-edit-done')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(config.hudLayout[HudMetric.speed]!.col, 0,
|
||||
reason: 'the rejected move must not change the persisted grid cell either');
|
||||
expect(tester.getTopLeft(speedFinder), before);
|
||||
});
|
||||
|
||||
testWidgets('a resize past the point of colliding with a sibling leaves the widget '
|
||||
'at its pre-gesture size', (tester) async {
|
||||
await tester.pumpWidget(host());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.longPress(find.byKey(const Key('hud-edit-background')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final speedFinder = find.byKey(const ValueKey(HudMetric.speed));
|
||||
final beforeSize = tester.getSize(speedFinder);
|
||||
final handleFinder = find.descendant(
|
||||
of: speedFinder,
|
||||
matching: find.byKey(const Key('hud-resize-handle')),
|
||||
);
|
||||
|
||||
// Speed defaults to colSpan 1 at col 0; growing it across the full grid width
|
||||
// would swallow avgSpeed/distance/elapsedTime's row-0 slots.
|
||||
final cellWidth = tester.view.physicalSize.width / tester.view.devicePixelRatio / hudGridColumns;
|
||||
final gesture = await tester.startGesture(tester.getCenter(handleFinder));
|
||||
await gesture.moveBy(Offset(cellWidth * hudGridColumns, 0));
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(const Key('hud-edit-done')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(config.hudLayout[HudMetric.speed]!.colSpan, 1,
|
||||
reason: 'the rejected resize must not change the persisted span either');
|
||||
expect(tester.getSize(speedFinder), beforeSize);
|
||||
});
|
||||
|
||||
testWidgets('HUD widget text never overflows at the smallest (1x1) allowed size',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: 400,
|
||||
height: 400,
|
||||
child: Stack(
|
||||
children: [
|
||||
DraggableResizableHudWidget(
|
||||
layout: const HudWidgetLayout(
|
||||
metric: HudMetric.speed,
|
||||
col: 0,
|
||||
row: 0,
|
||||
colSpan: hudMinSpan,
|
||||
rowSpan: hudMinSpan,
|
||||
visible: true,
|
||||
),
|
||||
areaSize: const Size(400, 400),
|
||||
editing: false,
|
||||
onMoved: (_, _) {},
|
||||
onResized: (_, _) {},
|
||||
child: _representativeHudChild(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'HUD widget text never overflows at the largest (max col/row span) allowed size',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: 400,
|
||||
height: 400,
|
||||
child: Stack(
|
||||
children: [
|
||||
DraggableResizableHudWidget(
|
||||
layout: const HudWidgetLayout(
|
||||
metric: HudMetric.speed,
|
||||
col: 0,
|
||||
row: 0,
|
||||
colSpan: hudMaxColSpan,
|
||||
rowSpan: hudMaxRowSpan,
|
||||
visible: true,
|
||||
),
|
||||
areaSize: const Size(400, 400),
|
||||
editing: false,
|
||||
onMoved: (_, _) {},
|
||||
onResized: (_, _) {},
|
||||
child: _representativeHudChild(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:rippr/src/config/config.dart';
|
||||
import 'package:rippr/src/hud/hud_layout_controller.dart';
|
||||
import 'package:rippr/src/hud/hud_metric.dart';
|
||||
import 'package:rippr/src/hud/hud_widget_layout.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
@@ -11,59 +12,123 @@ void main() {
|
||||
}
|
||||
|
||||
group('HudLayoutController', () {
|
||||
test('updatePosition updates only the given metric, clamped', () async {
|
||||
test('updatePosition updates only the given metric, clamped to the grid', () async {
|
||||
final controller = HudLayoutController(await freshConfig());
|
||||
final before = controller.state[HudMetric.distance]!;
|
||||
|
||||
controller.updatePosition(HudMetric.speed, 5.0, 5.0);
|
||||
// Row 4 is unused by any default layout, so this can't collide.
|
||||
controller.updatePosition(HudMetric.speed, 0, 4);
|
||||
|
||||
expect(controller.state[HudMetric.speed]!.x, lessThanOrEqualTo(1.0));
|
||||
expect(controller.state[HudMetric.distance]!.x, before.x,
|
||||
expect(controller.state[HudMetric.speed]!.row, 4);
|
||||
expect(controller.state[HudMetric.distance]!.col, before.col,
|
||||
reason: 'moving one metric must not disturb another');
|
||||
expect(controller.state[HudMetric.distance]!.row, before.row);
|
||||
});
|
||||
|
||||
test('updatePosition clamps a candidate that lands outside the grid', () async {
|
||||
final controller = HudLayoutController(await freshConfig());
|
||||
|
||||
controller.updatePosition(HudMetric.speed, 99, 99);
|
||||
|
||||
final speed = controller.state[HudMetric.speed]!;
|
||||
expect(speed.col, lessThanOrEqualTo(hudGridColumns - speed.colSpan));
|
||||
expect(speed.row, lessThanOrEqualTo(hudGridRows - speed.rowSpan));
|
||||
});
|
||||
|
||||
test('updateSize clamps to the legibility floor/ceiling', () async {
|
||||
final controller = HudLayoutController(await freshConfig());
|
||||
|
||||
controller.updateSize(HudMetric.speed, 0.0, 0.0);
|
||||
expect(controller.state[HudMetric.speed]!.width, greaterThan(0.0));
|
||||
expect(controller.state[HudMetric.speed]!.height, greaterThan(0.0));
|
||||
controller.updateSize(HudMetric.speed, 0, 0);
|
||||
expect(controller.state[HudMetric.speed]!.colSpan, hudMinSpan);
|
||||
expect(controller.state[HudMetric.speed]!.rowSpan, hudMinSpan);
|
||||
});
|
||||
|
||||
test('updatePosition onto a cell occupied by another visible metric is a no-op', () async {
|
||||
final controller = HudLayoutController(await freshConfig());
|
||||
final before = controller.state[HudMetric.speed]!;
|
||||
final avgSpeed = controller.state[HudMetric.avgSpeed]!;
|
||||
|
||||
// avgSpeed is visible by default -- try to drag speed directly on top of it.
|
||||
controller.updatePosition(HudMetric.speed, avgSpeed.col, avgSpeed.row);
|
||||
|
||||
expect(controller.state[HudMetric.speed]!.col, before.col);
|
||||
expect(controller.state[HudMetric.speed]!.row, before.row);
|
||||
});
|
||||
|
||||
test('updateSize that would overlap a sibling is a no-op', () async {
|
||||
final controller = HudLayoutController(await freshConfig());
|
||||
final before = controller.state[HudMetric.speed]!;
|
||||
|
||||
// Growing speed (default col 0, row 0) to span all 4 columns would swallow
|
||||
// avgSpeed/distance/elapsedTime's row-0 slots.
|
||||
controller.updateSize(HudMetric.speed, hudGridColumns, before.rowSpan);
|
||||
|
||||
expect(controller.state[HudMetric.speed]!.colSpan, before.colSpan);
|
||||
expect(controller.state[HudMetric.speed]!.rowSpan, before.rowSpan);
|
||||
});
|
||||
|
||||
test('setVisible(false) then setVisible(true) restores the last saved position, '
|
||||
'not a fresh default', () async {
|
||||
'when that position is still free', () async {
|
||||
final controller = HudLayoutController(await freshConfig());
|
||||
controller.updatePosition(HudMetric.avgSpeed, 0.6, 0.6);
|
||||
final movedX = controller.state[HudMetric.avgSpeed]!.x;
|
||||
// Row 4 is unused by any default layout, so this can't collide with anything.
|
||||
controller.updatePosition(HudMetric.avgSpeed, 0, 4);
|
||||
final moved = controller.state[HudMetric.avgSpeed]!;
|
||||
|
||||
controller.setVisible(HudMetric.avgSpeed, false);
|
||||
expect(controller.state[HudMetric.avgSpeed]!.visible, isFalse);
|
||||
|
||||
controller.setVisible(HudMetric.avgSpeed, true);
|
||||
expect(controller.state[HudMetric.avgSpeed]!.visible, isTrue);
|
||||
expect(controller.state[HudMetric.avgSpeed]!.x, movedX,
|
||||
expect(controller.state[HudMetric.avgSpeed]!.col, moved.col,
|
||||
reason: 'a metric with a real saved position keeps it when re-enabled');
|
||||
expect(controller.state[HudMetric.avgSpeed]!.row, moved.row);
|
||||
});
|
||||
|
||||
test('setVisible(true) when the metric\'s last position now collides with '
|
||||
'another visible widget places it somewhere else instead', () async {
|
||||
final controller = HudLayoutController(await freshConfig());
|
||||
final maxSpeedDefault = controller.state[HudMetric.maxSpeed]!;
|
||||
|
||||
// maxSpeed starts hidden at its default slot; move a currently-visible metric
|
||||
// onto that exact slot so it's occupied by the time maxSpeed is re-enabled.
|
||||
controller.updatePosition(
|
||||
HudMetric.speed,
|
||||
maxSpeedDefault.col,
|
||||
maxSpeedDefault.row,
|
||||
);
|
||||
|
||||
controller.setVisible(HudMetric.maxSpeed, true);
|
||||
|
||||
final maxSpeed = controller.state[HudMetric.maxSpeed]!;
|
||||
expect(maxSpeed.visible, isTrue);
|
||||
final others = controller.state.values.where(
|
||||
(l) => l.visible && l.metric != HudMetric.maxSpeed,
|
||||
);
|
||||
for (final other in others) {
|
||||
expect(hudRectsOverlap(maxSpeed, other), isFalse,
|
||||
reason: 'the re-enabled metric must not stack on any other visible widget');
|
||||
}
|
||||
});
|
||||
|
||||
test('persist writes the current state to Config', () async {
|
||||
final config = await freshConfig();
|
||||
final controller = HudLayoutController(config);
|
||||
controller.updatePosition(HudMetric.speed, 0.4, 0.4);
|
||||
controller.updatePosition(HudMetric.speed, 0, 4);
|
||||
|
||||
await controller.persist();
|
||||
|
||||
final reloaded = config.hudLayout;
|
||||
expect(reloaded[HudMetric.speed]!.x, 0.4);
|
||||
expect(reloaded[HudMetric.speed]!.row, 4);
|
||||
});
|
||||
|
||||
test('edits before persist are not visible to a fresh read of Config', () async {
|
||||
final config = await freshConfig();
|
||||
final controller = HudLayoutController(config);
|
||||
controller.updatePosition(HudMetric.speed, 0.4, 0.4);
|
||||
controller.updatePosition(HudMetric.speed, 0, 4);
|
||||
|
||||
// No persist() call yet.
|
||||
final reloaded = config.hudLayout;
|
||||
expect(reloaded[HudMetric.speed]!.x, isNot(0.4),
|
||||
expect(reloaded[HudMetric.speed]!.row, isNot(4),
|
||||
reason: 'a drag in progress must only touch in-memory state -- '
|
||||
'persisting every frame is the ticket\'s own named risk');
|
||||
});
|
||||
|
||||
@@ -2,53 +2,69 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:rippr/src/hud/hud_metric.dart';
|
||||
import 'package:rippr/src/hud/hud_widget_layout.dart';
|
||||
|
||||
/// UI-04: the JSON round-trip and clamp math are exercised directly with fixed inputs
|
||||
/// -- no real gestures needed to test either, per the ticket's own Tests section.
|
||||
/// FB-03: the JSON round-trip, grid-clamp math, overlap check, and free-slot search
|
||||
/// are all exercised directly with fixed inputs -- no real gestures needed to test
|
||||
/// any of these, per the ticket's own Tests section.
|
||||
void main() {
|
||||
group('JSON round trip', () {
|
||||
test('encodes and decodes exactly', () {
|
||||
const original = HudWidgetLayout(
|
||||
metric: HudMetric.speed,
|
||||
x: 0.1,
|
||||
y: 0.2,
|
||||
width: 0.3,
|
||||
height: 0.15,
|
||||
col: 1,
|
||||
row: 2,
|
||||
colSpan: 3,
|
||||
rowSpan: 2,
|
||||
visible: true,
|
||||
);
|
||||
|
||||
final decoded = HudWidgetLayout.fromJson(HudMetric.speed, original.toJson());
|
||||
|
||||
expect(decoded.x, original.x);
|
||||
expect(decoded.y, original.y);
|
||||
expect(decoded.width, original.width);
|
||||
expect(decoded.height, original.height);
|
||||
expect(decoded.col, original.col);
|
||||
expect(decoded.row, original.row);
|
||||
expect(decoded.colSpan, original.colSpan);
|
||||
expect(decoded.rowSpan, original.rowSpan);
|
||||
expect(decoded.visible, original.visible);
|
||||
});
|
||||
|
||||
test('a malformed entry falls back to the default rather than crashing', () {
|
||||
final decoded = HudWidgetLayout.fromJson(HudMetric.distance, {'x': 'not a number'});
|
||||
final decoded = HudWidgetLayout.fromJson(HudMetric.distance, {'col': 'not a number'});
|
||||
final default_ = HudWidgetLayout.defaultFor(HudMetric.distance);
|
||||
|
||||
expect(decoded.x, default_.x);
|
||||
expect(decoded.width, default_.width);
|
||||
expect(decoded.col, default_.col);
|
||||
expect(decoded.colSpan, default_.colSpan);
|
||||
});
|
||||
|
||||
test('a missing field falls back to the default rather than crashing', () {
|
||||
final decoded = HudWidgetLayout.fromJson(HudMetric.maxSpeed, {'x': 0.1, 'y': 0.1});
|
||||
final decoded = HudWidgetLayout.fromJson(HudMetric.maxSpeed, {'col': 1, 'row': 1});
|
||||
final default_ = HudWidgetLayout.defaultFor(HudMetric.maxSpeed);
|
||||
|
||||
expect(decoded.x, default_.x);
|
||||
expect(decoded.col, default_.col);
|
||||
});
|
||||
|
||||
test('an old-shaped (pre-grid) saved layout falls back to the default rather '
|
||||
'than crashing', () {
|
||||
final decoded = HudWidgetLayout.fromJson(
|
||||
HudMetric.speed,
|
||||
{'x': 0.1, 'y': 0.2, 'width': 0.3, 'height': 0.15, 'visible': true},
|
||||
);
|
||||
final default_ = HudWidgetLayout.defaultFor(HudMetric.speed);
|
||||
|
||||
expect(decoded.col, default_.col);
|
||||
expect(decoded.row, default_.row);
|
||||
expect(decoded.colSpan, default_.colSpan);
|
||||
expect(decoded.rowSpan, default_.rowSpan);
|
||||
});
|
||||
});
|
||||
|
||||
group('defaultFor', () {
|
||||
test('every metric gets a distinct default position', () {
|
||||
final positions = HudMetric.values
|
||||
.map(HudWidgetLayout.defaultFor)
|
||||
.map((l) => '${l.x},${l.y}')
|
||||
.toSet();
|
||||
expect(positions.length, HudMetric.values.length,
|
||||
reason: 'no two metrics should default to overlapping positions');
|
||||
test('every metric gets a distinct, non-overlapping default rect', () {
|
||||
final layouts = HudMetric.values.map(HudWidgetLayout.defaultFor).toList();
|
||||
for (var i = 0; i < layouts.length; i++) {
|
||||
for (var j = i + 1; j < layouts.length; j++) {
|
||||
expect(hudRectsOverlap(layouts[i], layouts[j]), isFalse,
|
||||
reason: 'no two metrics should default to overlapping rects');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('the first four metrics (the Map HUD mockup\'s fixed row: Speed/Avg Speed/'
|
||||
@@ -64,65 +80,146 @@ void main() {
|
||||
expect(HudWidgetLayout.defaultFor(HudMetric.elevationGain).visible, isFalse);
|
||||
});
|
||||
|
||||
test('every default is already within the valid clamp bounds', () {
|
||||
test('every default is already within the valid grid bounds', () {
|
||||
for (final metric in HudMetric.values) {
|
||||
final layout = HudWidgetLayout.defaultFor(metric);
|
||||
expect(layout.clamped().x, layout.x);
|
||||
expect(layout.clamped().y, layout.y);
|
||||
expect(layout.clamped().width, layout.width);
|
||||
expect(layout.clamped().height, layout.height);
|
||||
expect(layout.clampedToGrid().col, layout.col);
|
||||
expect(layout.clampedToGrid().row, layout.row);
|
||||
expect(layout.clampedToGrid().colSpan, layout.colSpan);
|
||||
expect(layout.clampedToGrid().rowSpan, layout.rowSpan);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('clamped', () {
|
||||
group('clampedToGrid', () {
|
||||
const base = HudWidgetLayout(
|
||||
metric: HudMetric.speed,
|
||||
x: 0.5,
|
||||
y: 0.5,
|
||||
width: 0.3,
|
||||
height: 0.15,
|
||||
col: 1,
|
||||
row: 1,
|
||||
colSpan: 2,
|
||||
rowSpan: 2,
|
||||
visible: true,
|
||||
);
|
||||
|
||||
test('a position dragged past the right/bottom edge is pulled back on-screen', () {
|
||||
final result = base.copyWith(x: 1.5, y: 1.5).clamped();
|
||||
expect(result.x, 1.0 - base.width);
|
||||
expect(result.y, 1.0 - base.height);
|
||||
test('a position dragged past the right/bottom edge is pulled back onto the '
|
||||
'grid', () {
|
||||
final result = base.copyWith(col: 20, row: 20).clampedToGrid();
|
||||
expect(result.col, hudGridColumns - base.colSpan);
|
||||
expect(result.row, hudGridRows - base.rowSpan);
|
||||
});
|
||||
|
||||
test('a position dragged past the left/top edge is pulled back on-screen', () {
|
||||
final result = base.copyWith(x: -0.5, y: -0.5).clamped();
|
||||
expect(result.x, 0.0);
|
||||
expect(result.y, 0.0);
|
||||
test('a position dragged past the left/top edge is pulled back onto the grid', () {
|
||||
final result = base.copyWith(col: -5, row: -5).clampedToGrid();
|
||||
expect(result.col, 0);
|
||||
expect(result.row, 0);
|
||||
});
|
||||
|
||||
test('a resize below the legibility floor is corrected up to the minimum', () {
|
||||
final result = base.copyWith(width: 0.01, height: 0.01).clamped();
|
||||
expect(result.width, hudMinWidthFraction);
|
||||
expect(result.height, hudMinHeightFraction);
|
||||
final result = base.copyWith(colSpan: 0, rowSpan: 0).clampedToGrid();
|
||||
expect(result.colSpan, hudMinSpan);
|
||||
expect(result.rowSpan, hudMinSpan);
|
||||
});
|
||||
|
||||
test('a resize above the sane ceiling is corrected down to the maximum', () {
|
||||
final result = base.copyWith(width: 5.0, height: 5.0).clamped();
|
||||
expect(result.width, hudMaxWidthFraction);
|
||||
expect(result.height, hudMaxHeightFraction);
|
||||
final result = base.copyWith(colSpan: 99, rowSpan: 99).clampedToGrid();
|
||||
expect(result.colSpan, hudMaxColSpan);
|
||||
expect(result.rowSpan, hudMaxRowSpan);
|
||||
});
|
||||
|
||||
test('shrinking to fit happens before repositioning, so a widget resized past '
|
||||
'the edge shrinks rather than silently relocates', () {
|
||||
final result = base.copyWith(x: 0.9, width: 0.7).clamped();
|
||||
expect(result.width, hudMaxWidthFraction);
|
||||
// x itself is still clamped against the (now-bounded) width so the widget
|
||||
// never sits partially off-screen either.
|
||||
expect(result.x + result.width, lessThanOrEqualTo(1.0));
|
||||
final result = base.copyWith(col: 3, colSpan: 4).clampedToGrid();
|
||||
expect(result.colSpan, hudMaxColSpan);
|
||||
// col itself is still clamped against the (now-bounded) span so the widget
|
||||
// never sits even partially off the grid either.
|
||||
expect(result.col + result.colSpan, lessThanOrEqualTo(hudGridColumns));
|
||||
});
|
||||
|
||||
test('an already-valid layout is unchanged', () {
|
||||
expect(base.clamped().x, base.x);
|
||||
expect(base.clamped().y, base.y);
|
||||
expect(base.clamped().width, base.width);
|
||||
expect(base.clamped().height, base.height);
|
||||
expect(base.clampedToGrid().col, base.col);
|
||||
expect(base.clampedToGrid().row, base.row);
|
||||
expect(base.clampedToGrid().colSpan, base.colSpan);
|
||||
expect(base.clampedToGrid().rowSpan, base.rowSpan);
|
||||
});
|
||||
});
|
||||
|
||||
group('hudRectsOverlap', () {
|
||||
const a = HudWidgetLayout(
|
||||
metric: HudMetric.speed,
|
||||
col: 1,
|
||||
row: 1,
|
||||
colSpan: 2,
|
||||
rowSpan: 2,
|
||||
visible: true,
|
||||
);
|
||||
|
||||
test('two identical rects overlap', () {
|
||||
expect(hudRectsOverlap(a, a), isTrue);
|
||||
});
|
||||
|
||||
test('two rects sharing only an edge (touching, not overlapping) do not overlap', () {
|
||||
final rightNeighbor = a.copyWith(col: a.col + a.colSpan);
|
||||
expect(hudRectsOverlap(a, rightNeighbor), isFalse);
|
||||
|
||||
final belowNeighbor = a.copyWith(row: a.row + a.rowSpan);
|
||||
expect(hudRectsOverlap(a, belowNeighbor), isFalse);
|
||||
});
|
||||
|
||||
test('two disjoint rects do not overlap', () {
|
||||
final far = a.copyWith(col: a.col + a.colSpan + 3, row: a.row + a.rowSpan + 3);
|
||||
expect(hudRectsOverlap(a, far), isFalse);
|
||||
});
|
||||
|
||||
test('a rect fully containing another overlaps', () {
|
||||
const big = HudWidgetLayout(
|
||||
metric: HudMetric.distance,
|
||||
col: 0,
|
||||
row: 0,
|
||||
colSpan: 4,
|
||||
rowSpan: 4,
|
||||
visible: true,
|
||||
);
|
||||
expect(hudRectsOverlap(big, a), isTrue);
|
||||
});
|
||||
|
||||
test('two partially-overlapping rects overlap', () {
|
||||
final overlapping = a.copyWith(col: a.col + 1, row: a.row + 1);
|
||||
expect(hudRectsOverlap(a, overlapping), isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('nextFreeSlot', () {
|
||||
test('returns a rect that does not collide with any occupied visible layout, '
|
||||
'when the metric\'s own default position is taken', () {
|
||||
final occupier = HudWidgetLayout.defaultFor(HudMetric.speed);
|
||||
final occupied = {HudMetric.speed: occupier};
|
||||
|
||||
final result = HudWidgetLayout.nextFreeSlot(
|
||||
HudMetric.avgSpeed,
|
||||
occupied: occupied,
|
||||
colSpan: occupier.colSpan,
|
||||
rowSpan: occupier.rowSpan,
|
||||
);
|
||||
|
||||
expect(hudRectsOverlap(result, occupier), isFalse);
|
||||
});
|
||||
|
||||
test('returns the same rect defaultFor would have (or another valid one) when '
|
||||
'occupied is empty', () {
|
||||
final result = HudWidgetLayout.nextFreeSlot(HudMetric.speed, occupied: {});
|
||||
expect(result.col, 0);
|
||||
expect(result.row, 0);
|
||||
});
|
||||
|
||||
test('returns the same rect defaultFor would have (or another valid one) when '
|
||||
'occupied is all-hidden', () {
|
||||
final hidden = HudWidgetLayout.defaultFor(HudMetric.speed).copyWith(visible: false);
|
||||
final result = HudWidgetLayout.nextFreeSlot(
|
||||
HudMetric.avgSpeed,
|
||||
occupied: {HudMetric.speed: hidden},
|
||||
);
|
||||
expect(result.col, 0);
|
||||
expect(result.row, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user