Dash: DIN 1451 opt-in per widget (hudFont), GNSS compass, rei by default; widget catalogue
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:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user