FB-09: size default HUD widgets by title, not a fixed uniform cell
Adds minColSpanForLabel as a character-count proxy for how many grid columns a metric's title needs, reworks HudWidgetLayout.defaultFor into a left-to-right row-packing layout using it, enforces the same minimum as a floor on manual resize, and splits _HudMetricValue's FittedBox so the title renders at a fixed reference size while only the value shrinks/grows to fill the remaining space. Confirms a real, documented interaction with FB-08's _reflowRow: its even column split has no awareness of a member's title-driven minimum and can push one below it. Left unfixed per this ticket's own Out-of-scope section -- _reflowRow's algorithm is FB-08's, not this ticket's, to change. Test count 426 -> 434, all green. flutter analyze unchanged (same 4 pre-existing, unrelated info-level issues).
This commit is contained in:
@@ -10,20 +10,33 @@ 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)
|
||||
/// A representative HUD child -- structurally the same fixed-title/flexible-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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
/// referenced directly from outside its library. FB-09: the title is a plain,
|
||||
/// unwrapped `Text` at a fixed reference size; only the value is wrapped in its own
|
||||
/// `Flexible(child: FittedBox(...))`.
|
||||
Widget _representativeHudChild({String label = 'SPEED', String value = '12.3 km/h'}) =>
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 10, letterSpacing: 1),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Flexible(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
value,
|
||||
style: const 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
|
||||
@@ -263,4 +276,143 @@ void main() {
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'resizing a widget below its title\'s minimum colSpan via the resize handle '
|
||||
'clamps to that minimum, not the generic hudMinSpan', (tester) async {
|
||||
// Points captured is the longest label -- minColSpanForLabel gives it 3, well
|
||||
// above the generic hudMinSpan of 1.
|
||||
const metric = HudMetric.pointsCaptured;
|
||||
final expectedMin = minColSpanForLabel(metric.label);
|
||||
expect(expectedMin, greaterThan(hudMinSpan));
|
||||
|
||||
int? resizedColSpan;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: 400,
|
||||
height: 400,
|
||||
child: Stack(
|
||||
children: [
|
||||
DraggableResizableHudWidget(
|
||||
layout: HudWidgetLayout(
|
||||
metric: metric,
|
||||
col: 0,
|
||||
row: 0,
|
||||
colSpan: expectedMin,
|
||||
rowSpan: 2,
|
||||
visible: true,
|
||||
),
|
||||
areaSize: const Size(400, 400),
|
||||
editing: true,
|
||||
onMoved: (_, _) {},
|
||||
onResized: (colSpan, _) => resizedColSpan = colSpan,
|
||||
child: _representativeHudChild(label: metric.label.toUpperCase()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final handleFinder = find.byKey(const Key('hud-resize-handle'));
|
||||
final gesture = await tester.startGesture(tester.getCenter(handleFinder));
|
||||
// Drag far enough left/up to try to shrink well below every floor.
|
||||
await gesture.moveBy(const Offset(-390, -390));
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(resizedColSpan, expectedMin,
|
||||
reason: 'the resize handle must clamp up to the title\'s own minimum, not '
|
||||
'the generic hudMinSpan');
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'the longest label renders in full, without overflow, at exactly its own '
|
||||
'title-driven minimum colSpan', (tester) async {
|
||||
const metric = HudMetric.pointsCaptured;
|
||||
final minSpan = minColSpanForLabel(metric.label);
|
||||
final cellWidth = 400 / hudGridColumns;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: 400,
|
||||
height: 400,
|
||||
child: Stack(
|
||||
children: [
|
||||
DraggableResizableHudWidget(
|
||||
layout: HudWidgetLayout(
|
||||
metric: metric,
|
||||
col: 0,
|
||||
row: 0,
|
||||
colSpan: minSpan,
|
||||
rowSpan: 2,
|
||||
visible: true,
|
||||
),
|
||||
areaSize: const Size(400, 400),
|
||||
editing: false,
|
||||
onMoved: (_, _) {},
|
||||
onResized: (_, _) {},
|
||||
child: _representativeHudChild(label: metric.label.toUpperCase()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.text('POINTS CAPTURED'), findsOneWidget,
|
||||
reason: 'the full title must be present, not an ellipsized fragment, at its '
|
||||
'own minimum width ($minSpan cols * $cellWidth px each)');
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'the title\'s rendered font size never grows past its fixed reference size, '
|
||||
'while the value is free to size differently', (tester) async {
|
||||
// Pumped directly inside a tightly-sized SizedBox (rather than through
|
||||
// DraggableResizableHudWidget's GlassPanel/Center/AnimatedScale chain, which
|
||||
// hands the content loose rather than tight constraints) so the Column's
|
||||
// Flexible-wrapped value is actually forced to size against the box, exercising
|
||||
// the fixed-title/flexible-value split this ticket adds.
|
||||
Widget host(Size size) => MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
child: _representativeHudChild(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// FittedBox scales its child at paint time via a transform, not by resizing the
|
||||
// child's own layout box -- so the inner value Text's RenderBox size is always
|
||||
// its unscaled natural size, regardless of how much the FittedBox actually
|
||||
// shrank it visually. The FittedBox's own rendered size is what reflects the
|
||||
// available space, so that -- not the Text inside it -- is what's compared here.
|
||||
await tester.pumpWidget(host(const Size(60, 40)));
|
||||
await tester.pumpAndSettle();
|
||||
final smallTitleFontSize = tester.widget<Text>(find.text('SPEED')).style?.fontSize;
|
||||
final smallValueBoxSize = tester.getSize(find.byType(FittedBox));
|
||||
|
||||
await tester.pumpWidget(host(const Size(600, 400)));
|
||||
await tester.pumpAndSettle();
|
||||
final bigTitleFontSize = tester.widget<Text>(find.text('SPEED')).style?.fontSize;
|
||||
final bigValueBoxSize = tester.getSize(find.byType(FittedBox));
|
||||
|
||||
expect(bigTitleFontSize, smallTitleFontSize,
|
||||
reason: 'the title is a fixed-size Text, not wrapped in a FittedBox, so a '
|
||||
'much larger widget must not change its style\'s font size');
|
||||
expect(bigValueBoxSize, isNot(smallValueBoxSize),
|
||||
reason: 'the value is the flexible part and its FittedBox is free to be '
|
||||
'sized differently as the widget grows');
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user