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
+3
View File
@@ -126,6 +126,7 @@ class _AccelGraphState extends State<AccelGraph> {
Text(
'Lon: ${_formatAccel(widget.ay)} (${_formatAccel(_ghostAy)})',
style: TextStyle(
fontFamily: hudFont,
fontSize: fontSize * 0.5,
fontWeight: FontWeight.w400,
fontFeatures: const [FontFeature.tabularFigures()],
@@ -136,6 +137,7 @@ class _AccelGraphState extends State<AccelGraph> {
Text(
'Lat: ${_formatAccel(widget.ax)} (${_formatAccel(_ghostAx)})',
style: TextStyle(
fontFamily: hudFont,
fontSize: fontSize * 0.5,
fontWeight: FontWeight.w400,
fontFeatures: const [FontFeature.tabularFigures()],
@@ -149,6 +151,7 @@ class _AccelGraphState extends State<AccelGraph> {
Text(
'Acceleration',
style: TextStyle(
fontFamily: hudFont,
fontSize: fontSize * 0.8,
fontWeight: FontWeight.w400,
color: theme.subdued,
+94
View File
@@ -0,0 +1,94 @@
// Ported from smart-serow (pi/ui/lib/widgets/gps_compass.dart), itself from android-stuff/gps-speedo:
// a heading arrow and its bearing. Changes: the palette import and lint fixes.
import 'dart:math' as math;
import 'package:flutter/material.dart';
import '../theme/palette.dart';
class GpsCompass extends StatelessWidget {
final double? heading;
final String? gpsState; // "acquiring", "fix", "lost"
const GpsCompass({super.key, this.heading, this.gpsState});
bool get _hasSignal => heading != null;
bool get _isAcquiring => gpsState == 'acquiring';
String get _displayHeading {
if (!_hasSignal) return 'N/A';
return '${(heading! % 360).round()}';
}
String get _compassDirection {
if (!_hasSignal) return '';
final directions = [
'N',
'NNE',
'NE',
'ENE',
'E',
'ESE',
'SE',
'SSE',
'S',
'SSW',
'SW',
'WSW',
'W',
'WNW',
'NW',
'NNW',
];
final index = ((heading! % 360) / 22.5).round() % 16;
return directions[index];
}
@override
Widget build(BuildContext context) {
final theme = AppTheme.of(context);
// No signal = subdued color, valid = foreground
final iconColour = _hasSignal ? theme.foreground : theme.highlight;
// Convert to radians, 0 = no rotation when no signal
final angle = _hasSignal ? (heading! * math.pi / 180.0) : 0.0;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
flex: 3,
child: Transform.rotate(
angle: _hasSignal ? angle : 0,
child: FittedBox(
fit: BoxFit.contain,
child: Icon(
_hasSignal ? Icons.navigation : Icons.navigation_outlined,
size: 120,
color: iconColour,
),
),
),
),
Flexible(
flex: 1,
child: FittedBox(
fit: BoxFit.contain,
child: Text(
_hasSignal
? "$_displayHeading $_compassDirection"
: (_isAcquiring ? "ACQ" : "N/A"),
style: TextStyle(
fontSize: 80,
color: theme.subdued, // less emphasis on text, let the icon have semantic colour
fontFamily: hudFont,
),
),
),
),
],
);
}
}
+98 -78
View File
@@ -50,52 +50,62 @@ class StatBox extends StatelessWidget {
builder: (context, constraints) {
final baseSize = constraints.maxHeight * 0.4;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Value + optional unit
Row(
// Shrinks (never wraps) when a label is wider than its box.
return Center(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
// Value + optional unit
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
value,
style: TextStyle(
fontFamily: hudFont,
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(
fontFamily: hudFont,
fontSize: baseSize * 0.5,
fontWeight: FontWeight.w400,
color: theme.subdued,
height: 1,
),
),
],
],
),
SizedBox(height: baseSize * 0.1),
// Label
Text(
value,
label,
style: TextStyle(
fontSize: baseSize,
fontFamily: hudFont,
fontSize: baseSize * 0.6,
fontWeight: FontWeight.w400,
fontFeatures: const [FontFeature.tabularFigures()],
color: valueColor,
height: 1,
color: theme.subdued,
letterSpacing: 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,
),
),
],
),
);
},
),
@@ -134,55 +144,65 @@ class StatBoxMain extends StatelessWidget {
// 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(
// Shrinks (never wraps) when a label is wider than its box.
return Center(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
// Value + optional unit row
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
value,
style: TextStyle(
fontFamily: hudFont,
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(
fontFamily: hudFont,
fontSize: baseSize * 0.4,
fontWeight: FontWeight.w400,
color: theme.subdued,
height: 1,
),
),
],
],
),
SizedBox(height: baseSize * 0.1),
// Label
Text(
value,
label,
style: TextStyle(
fontSize: baseSize,
fontWeight: FontWeight.w300,
fontFeatures: const [FontFeature.tabularFigures()],
color: dim
? theme.subdued
: warning
? theme.highlight
: theme.foreground,
height: 1,
fontFamily: hudFont,
fontSize: baseSize * 0.35,
fontWeight: FontWeight.w400,
color: theme.subdued,
letterSpacing: 2,
),
),
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,
),
),
],
),
);
},
),
+72
View File
@@ -0,0 +1,72 @@
// An on/off lamp in the boxed pattern (lib/widgets/stat_box.dart): the label is the
// lamp. Takes a [Reading] (lib/reading.dart) so missing and stale draw as they do elsewhere.
import 'package:flutter/material.dart';
import '../reading.dart';
import '../theme/palette.dart';
/// 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: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
missing ? '—' : label,
maxLines: 1,
style: TextStyle(
fontFamily: hudFont,
fontSize: size,
fontWeight: lit ? FontWeight.w700 : FontWeight.w400,
color: color,
letterSpacing: 1,
),
),
),
);
},
),
);
}
}
+3
View File
@@ -64,6 +64,7 @@ class WhiskeyMark extends StatelessWidget {
Text(
'Roll: ${_formatAngle(roll)}',
style: TextStyle(
fontFamily: hudFont,
fontSize: fontSize * 0.5,
fontWeight: FontWeight.w400,
fontFeatures: const [FontFeature.tabularFigures()],
@@ -74,6 +75,7 @@ class WhiskeyMark extends StatelessWidget {
Text(
'Pitch: ${_formatAngle(pitch)}',
style: TextStyle(
fontFamily: hudFont,
fontSize: fontSize * 0.5,
fontWeight: FontWeight.w400,
fontFeatures: const [FontFeature.tabularFigures()],
@@ -87,6 +89,7 @@ class WhiskeyMark extends StatelessWidget {
Text(
'Attitude',
style: TextStyle(
fontFamily: hudFont,
fontSize: fontSize * 0.8,
fontWeight: FontWeight.w400,
color: theme.subdued,