Dash: DIN 1451 opt-in per widget (hudFont), GNSS compass, rei by default; widget catalogue
/ crosscheck (push) Successful in 1s
CI / Analyze & Test (push) Successful in 14s
CI / Debug APK (push) Successful in 36s

From smart-serow: the DIN 1451 face (Alte DIN 1451 Mittelschrift, via gps-speedo)
and GpsCompass. A cosmetic font is opt-in: widgets name hudFont in their own
TextStyle, the rest keeps the basic font (CJK). Boxes and telltales shrink long
labels instead of wrapping. Telltale and Reading get their own files;
lib/README.md catalogues folders and widgets like smart-serow's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
This commit is contained in:
2026-10-14 13:20:34 +09:00
parent 728804e971
commit 8d95514897
11 changed files with 360 additions and 167 deletions
+16 -84
View File
@@ -6,35 +6,14 @@
import 'package:flutter/material.dart';
import 'boundary.dart';
import 'reading.dart';
import 'theme/palette.dart';
import 'widgets/accel_graph.dart';
import 'widgets/gps_compass.dart';
import 'widgets/stat_box.dart';
import 'widgets/telltale.dart';
import 'widgets/whiskey_mark.dart';
/// One signal's current state, as the dash needs it.
class Reading {
const Reading(this.value, {this.dim = false});
/// The value, or null when missing or known-without-value.
final Object? value;
/// Missing or stale: drawn in subdued.
final bool dim;
static Reading of(Boundary b, String name) {
final e = b.signals[name];
if (e is! Map) return const Reading(null, dim: true);
final q = e['quality'] as String? ?? 'ok';
return Reading(e['value'], dim: q != 'ok' || e['value'] == null);
}
double? get number => value is num ? (value as num).toDouble() : null;
bool get on => value == true;
String fixed([int decimals = 0]) =>
number == null ? '—' : number!.toStringAsFixed(decimals);
}
class DashPage extends StatelessWidget {
const DashPage({super.key, required this.boundary, required this.themes});
final Boundary boundary;
@@ -54,6 +33,13 @@ class DashPage extends StatelessWidget {
final coolant = r('obd_coolant_temp_c');
final lat = r('imu_accel_lat_g');
final lon = r('imu_accel_lon_g');
// The compass's states, from gnss_fix: none → acquiring, 2d/3d → fix, missing → none known.
final fix = r('gnss_fix');
final gpsState = fix.value == null
? null
: fix.value == 'none'
? 'acquiring'
: 'fix';
return Scaffold(
backgroundColor: p.background,
@@ -139,6 +125,12 @@ class DashPage extends StatelessWidget {
dim: coolant.dim,
isWarning: () => (coolant.number ?? 0) > 105,
),
Expanded(
child: GpsCompass(
heading: r('gnss_heading_deg').number,
gpsState: gpsState,
),
),
Telltale(
label: 'LEFT',
icon: Icons.arrow_left,
@@ -183,66 +175,6 @@ class DashPage extends StatelessWidget {
}
}
/// An on/off lamp in the boxed pattern: the label is the lamp. Lit in foreground
/// (highlight when [warn]), dark in subdued; missing shows as a dash.
class Telltale extends StatelessWidget {
const Telltale({
super.key,
required this.label,
required this.reading,
this.icon,
this.warn = false,
this.flex = 1,
});
final String label;
/// Drawn instead of the label when set. Glyphs like ◀ ▶ come out as emoji on Android.
final IconData? icon;
final Reading reading;
final bool warn;
final int flex;
@override
Widget build(BuildContext context) {
final p = AppTheme.of(context);
final missing = reading.value == null;
final lit = reading.on && !reading.dim;
final color = lit
? (warn ? p.highlight : p.foreground)
: p.subdued.withValues(alpha: missing ? 0.35 : 0.6);
return Expanded(
flex: flex,
child: LayoutBuilder(
builder: (context, c) {
final size = c.maxHeight * 0.3;
if (icon != null && !missing) {
return Center(
child: Icon(
icon,
size: c.maxHeight * 0.7,
color: color,
semanticLabel: label,
),
);
}
return Center(
child: Text(
missing ? '—' : label,
style: TextStyle(
fontSize: size,
fontWeight: lit ? FontWeight.w700 : FontWeight.w400,
color: color,
letterSpacing: 1,
),
),
);
},
),
);
}
}
/// The dash's own thin top line: link state, back, and the dark/bright switch.
class _DashBar extends StatelessWidget {
const _DashBar({required this.boundary, required this.themes});