Files
app-flutter/lib/widgets/stat_box.dart
T
mikkeli b083cf31dd
/ crosscheck (push) Successful in 1s
CI / Analyze & Test (push) Successful in 9s
CI / Debug APK (push) Successful in 17s
Dash page and smart-serow's palette, boxed widgets, attitude and G-meter
Ported from smart-serow's HUD: the four-colour palette with dark/bright variants
(same theme JSON format, loaded at runtime; YUKART_THEME picks one), StatBox and
StatBoxMain, WhiskeyMark and AccelGraph. The whole app takes its colours from the
palette. Dash: attitude, G-meter, RPM, GNSS speed, pedal, coolant and telltales,
each from its contracts signal; missing and stale draw in subdued.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 23:25:28 +09:00

192 lines
5.7 KiB
Dart

// The boxed-widget pattern from smart-serow's HUD (pi/ui/lib/widgets/stat_box*.dart):
// every readout is a box that takes its share of a row (`flex`) and sizes its type
// from its own height, so a layout is a grid of boxes, and a value changing digits
// never moves anything. Value in foreground with tabular figures, unit and label in
// subdued; a warning turns the value to highlight.
//
// Added for Yukart: [dim]. A missing or stale value is a normal state in a car
// (app guideline 2), shown in subdued rather than hidden.
import 'package:flutter/material.dart';
import '../theme/palette.dart';
/// A labeled stat box: the small one, for rows of readouts.
class StatBox extends StatelessWidget {
const StatBox({
super.key,
required this.value,
this.unit,
required this.label,
this.flex = 1,
this.isWarning,
this.dim = false,
});
final String value;
final String? unit;
final String label;
final int flex;
/// If it returns true, the value shows in highlight.
final bool Function()? isWarning;
/// Missing or stale: the value shows in subdued.
final bool dim;
@override
Widget build(BuildContext context) {
final theme = AppTheme.of(context);
final warning = isWarning?.call() ?? false;
final valueColor = dim
? theme.subdued
: warning
? theme.highlight
: theme.foreground;
return Expanded(
flex: flex,
child: LayoutBuilder(
builder: (context, constraints) {
final baseSize = constraints.maxHeight * 0.4;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Value + optional unit
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
value,
style: TextStyle(
fontSize: baseSize,
fontWeight: FontWeight.w400,
fontFeatures: const [FontFeature.tabularFigures()],
color: valueColor,
height: 1,
),
),
SizedBox(width: baseSize * 0.1),
if (unit != null) ...[
const SizedBox(width: 4),
Text(
unit!,
style: TextStyle(
fontSize: baseSize * 0.5,
fontWeight: FontWeight.w400,
color: theme.subdued,
height: 1,
),
),
],
],
),
SizedBox(height: baseSize * 0.1),
// Label
Text(
label,
style: TextStyle(
fontSize: baseSize * 0.6,
fontWeight: FontWeight.w400,
color: theme.subdued,
letterSpacing: 1,
),
),
],
);
},
),
);
}
}
/// The large stat box, for the main area. Fixed-size: content changes don't move the layout.
class StatBoxMain extends StatelessWidget {
const StatBoxMain({
super.key,
required this.value,
this.unit,
required this.label,
this.flex = 1,
this.isWarning,
this.dim = false,
});
final String value;
final String? unit;
final String label;
final int flex;
final bool Function()? isWarning;
final bool dim;
@override
Widget build(BuildContext context) {
final theme = AppTheme.of(context);
final warning = isWarning?.call() ?? false;
return Expanded(
flex: flex,
child: LayoutBuilder(
builder: (context, constraints) {
// Scale fonts relative to box height for consistent proportions
final baseSize = constraints.maxHeight * 0.4;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Value + optional unit row
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
value,
style: TextStyle(
fontSize: baseSize,
fontWeight: FontWeight.w300,
fontFeatures: const [FontFeature.tabularFigures()],
color: dim
? theme.subdued
: warning
? theme.highlight
: theme.foreground,
height: 1,
),
),
if (unit != null) ...[
const SizedBox(width: 8),
Text(
unit!,
style: TextStyle(
fontSize: baseSize * 0.4,
fontWeight: FontWeight.w400,
color: theme.subdued,
height: 1,
),
),
],
],
),
SizedBox(height: baseSize * 0.1),
// Label
Text(
label,
style: TextStyle(
fontSize: baseSize * 0.35,
fontWeight: FontWeight.w400,
color: theme.subdued,
letterSpacing: 2,
),
),
],
);
},
),
);
}
}