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:
@@ -20,6 +20,17 @@ const int hudMaxRowSpan = 3;
|
||||
|
||||
/// True if rectangles [a] and [b] (both in grid-cell coordinates) overlap -- edges that
|
||||
/// merely touch do not count as overlapping.
|
||||
/// A label under 10 characters fits one column. A label under 15 characters needs
|
||||
/// two. Anything longer needs three. This is a character-count proxy for "how much
|
||||
/// horizontal room this title needs," not an exact pixel measurement -- `defaultFor`
|
||||
/// runs with no `BuildContext` and cannot measure real text width. Adjust these
|
||||
/// thresholds directly if a future label reads too cramped or too loose in practice.
|
||||
int minColSpanForLabel(String label) {
|
||||
if (label.length < 10) return 1;
|
||||
if (label.length < 15) return 2;
|
||||
return 3;
|
||||
}
|
||||
|
||||
bool hudRectsOverlap(HudWidgetLayout a, HudWidgetLayout b) {
|
||||
final aColEnd = a.col + a.colSpan;
|
||||
final aRowEnd = a.row + a.rowSpan;
|
||||
@@ -113,26 +124,41 @@ class HudWidgetLayout {
|
||||
/// A deterministic starting grid -- a fresh install has a working, if plain, HUD
|
||||
/// before the rider customises anything, and a metric toggled on for the first time
|
||||
/// (with no saved position) lands somewhere sane rather than stacked on another
|
||||
/// widget. Two rows of four, matching the Stitch mockup's row of cards for however
|
||||
/// many metrics fit in the first row, with the rest continuing below it. Purely a
|
||||
/// function of the metric's own index -- these 8 fixed slots never overlap each
|
||||
/// other by construction, so no runtime state is needed here.
|
||||
/// widget. FB-09: rather than a fixed "always 4 per row, colSpan 1" assumption, this
|
||||
/// packs metrics left-to-right with each one's own title-driven width
|
||||
/// ([minColSpanForLabel]), wrapping to a new row when the current one would
|
||||
/// overflow. Purely a function of the metric's own index/label -- these slots never
|
||||
/// overlap each other by construction, so no runtime state is needed here.
|
||||
factory HudWidgetLayout.defaultFor(HudMetric metric) {
|
||||
const columns = 4;
|
||||
const rowSpan = 2;
|
||||
final index = HudMetric.values.indexOf(metric);
|
||||
final row = (index ~/ columns) * rowSpan;
|
||||
final col = index % columns;
|
||||
return HudWidgetLayout(
|
||||
metric: metric,
|
||||
col: col,
|
||||
row: row,
|
||||
colSpan: 1,
|
||||
rowSpan: rowSpan,
|
||||
// The Map HUD mockup's own fixed row is Speed/Avg Speed/Dist/Time -- the first
|
||||
// four enum values are ordered to match, so only those start visible.
|
||||
visible: index < columns,
|
||||
);
|
||||
var col = 0;
|
||||
var row = 0;
|
||||
for (final m in HudMetric.values) {
|
||||
final span = minColSpanForLabel(m.label);
|
||||
if (col + span > hudGridColumns) {
|
||||
col = 0;
|
||||
row += rowSpan;
|
||||
}
|
||||
if (m == metric) {
|
||||
return HudWidgetLayout(
|
||||
metric: metric,
|
||||
col: col,
|
||||
row: row,
|
||||
colSpan: span,
|
||||
rowSpan: rowSpan,
|
||||
// The Map HUD mockup's own fixed row is Speed/Avg Speed/Dist/Time -- the
|
||||
// first four enum values are ordered to match, so only those start visible.
|
||||
// Their exact column positions may no longer form four perfectly even
|
||||
// columns once their individual widths differ -- that is the correct,
|
||||
// intended result of sizing by title, not a bug (see FB-09's Outcome
|
||||
// section).
|
||||
visible: HudMetric.values.indexOf(metric) < 4,
|
||||
);
|
||||
}
|
||||
col += span;
|
||||
}
|
||||
// Unreachable: metric is always one of HudMetric.values.
|
||||
throw StateError('Unknown metric: $metric');
|
||||
}
|
||||
|
||||
/// Scans row-major from (0,0) for the first [colSpan]x[rowSpan] slot that doesn't
|
||||
|
||||
@@ -146,9 +146,18 @@ class _DraggableResizableHudWidgetState extends State<DraggableResizableHudWidge
|
||||
});
|
||||
},
|
||||
onPanEnd: (_) {
|
||||
// FB-09: a rider can still make a widget larger than its title
|
||||
// needs (for a bigger value reading), but never smaller than the
|
||||
// title's own minimum -- clamped here rather than left to the
|
||||
// generic hudMinSpan floor in HudLayoutController.
|
||||
final snappedColSpan =
|
||||
((layout.colSpan * _cellWidth + _resizeDelta.width) / _cellWidth)
|
||||
.round();
|
||||
(((layout.colSpan * _cellWidth + _resizeDelta.width) /
|
||||
_cellWidth)
|
||||
.round())
|
||||
.clamp(
|
||||
minColSpanForLabel(layout.metric.label),
|
||||
hudMaxColSpan,
|
||||
);
|
||||
final snappedRowSpan =
|
||||
((layout.rowSpan * _cellHeight + _resizeDelta.height) /
|
||||
_cellHeight)
|
||||
|
||||
@@ -328,38 +328,41 @@ class _HudMetricValue extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
// FB-03: FittedBox owns sizing here -- structurally impossible to overflow, so
|
||||
// maxLines/overflow are dropped from both Texts. fontSize: 10/18 below (still
|
||||
// scaled by V3-05/FB-02's mounted-mode [scale]) are now just the "reference" size
|
||||
// FittedBox scales down from at small widget sizes; the ratio between label and
|
||||
// value size is preserved automatically as it scales. Note: BoxFit.scaleDown never
|
||||
// enlarges past that reference size, so a widget resized to the grid's maximum
|
||||
// span still shows the same reference text, just with more empty space around it
|
||||
// -- not larger text filling the space.
|
||||
return FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
metric.label.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 10 * scale,
|
||||
letterSpacing: 1,
|
||||
color: colors.onSurfaceVariant,
|
||||
// FB-09: the title is a plain, unwrapped Text at its fixed reference size -- it
|
||||
// must always fit at a readable size, never shrinking along with the value. Only
|
||||
// the value is wrapped in its own FittedBox, inside a Flexible so it claims
|
||||
// whatever vertical space is left after the fixed-size title rather than both
|
||||
// competing for space inside one shared FittedBox as before. maxLines/overflow are
|
||||
// restored on the title as a safety net in case a future label is ever added that
|
||||
// minColSpanForLabel's thresholds under-estimate.
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
metric.label.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 10 * scale,
|
||||
letterSpacing: 1,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Flexible(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
_value,
|
||||
style: monoDigits.copyWith(
|
||||
fontSize: 18 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _valueColor(colors),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_value,
|
||||
style: monoDigits.copyWith(
|
||||
fontSize: 18 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _valueColor(colors),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user