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:
Binary file not shown.
@@ -0,0 +1,34 @@
|
||||
---
|
||||
type: reference
|
||||
---
|
||||
# The app's code: a catalogue
|
||||
|
||||
Laid out like smart-serow's HUD (`pi/ui/lib/`), which several pieces come from.
|
||||
|
||||
## Folders
|
||||
|
||||
| Where | Holds |
|
||||
|---|---|
|
||||
| `lib/*.dart` | Pages (`dash`, `checklist`, `camview`, `linktest`), the boundary client (`boundary`), the USB network binding (`net`), and `reading`, one signal's state as a display needs it |
|
||||
| `theme/` | `palette.dart`: the palette and `hudFont` |
|
||||
| `widgets/` | Reusable display widgets, one per file, each fed values, never the boundary |
|
||||
|
||||
## Widgets
|
||||
|
||||
| Widget | File | Shows | From |
|
||||
|---|---|---|---|
|
||||
| `StatBox`, `StatBoxMain` | `widgets/stat_box.dart` | A value, its unit and label, sized from its box's height; highlight on warning, subdued when dim | smart-serow |
|
||||
| `Telltale` | `widgets/telltale.dart` | An on/off lamp: a label or an icon, lit or dark, "—" when missing | Yukart |
|
||||
| `WhiskeyMark` | `widgets/whiskey_mark.dart` | Attitude: a horizon tilting with roll, ladder moving with pitch | smart-serow |
|
||||
| `AccelGraph` | `widgets/accel_graph.dart` | G-meter: a dot for lateral/longitudinal g, a trace and a peak marker over a window | smart-serow |
|
||||
| `GpsCompass` | `widgets/gps_compass.dart` | Heading arrow and bearing; "ACQ" while acquiring, "N/A" without | smart-serow, from gps-speedo |
|
||||
|
||||
## Conventions
|
||||
|
||||
- **The boxed pattern.** A widget takes its share of a row and sizes its type from its own height, so a layout
|
||||
is a grid of boxes and a value gaining a digit never moves anything. Labels shrink to fit, never wrap.
|
||||
- **Colours by meaning.** `AppTheme.of(context)` gives the palette's `background`, `foreground`, `highlight` and
|
||||
`subdued`; no widget names a colour. Themes are smart-serow's JSON (`assets/themes/`), `rei` by default.
|
||||
- **Cosmetic fonts are opt-in.** A widget that wants the HUD face names `fontFamily: hudFont` in its own
|
||||
`TextStyle`; everything else keeps the basic font, which covers CJK where DIN 1451 does not.
|
||||
- **Missing is a state** (app guideline 2): a widget given no value draws "—" or subdued, never an error.
|
||||
+16
-84
@@ -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});
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// One signal's current state as a display needs it: value, and whether to draw it dim
|
||||
// (missing, stale or known-without-value; app guideline 2).
|
||||
|
||||
import 'boundary.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);
|
||||
}
|
||||
@@ -6,17 +6,18 @@
|
||||
// final p = AppTheme.of(context);
|
||||
// color: p.foreground
|
||||
//
|
||||
// Pick the theme at build time: `--dart-define=YUKART_THEME=rei` (default: default).
|
||||
// Pick the theme at build time: `--dart-define=YUKART_THEME=default` (default: rei, smart-serow's).
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
const themeName = String.fromEnvironment(
|
||||
'YUKART_THEME',
|
||||
defaultValue: 'default',
|
||||
);
|
||||
/// The HUD face (smart-serow's DIN 1451). Opt-in: a widget names it in its own
|
||||
/// TextStyle; everything else keeps the basic font, which covers CJK where DIN does not.
|
||||
const hudFont = 'DIN1451';
|
||||
|
||||
const themeName = String.fromEnvironment('YUKART_THEME', defaultValue: 'rei');
|
||||
|
||||
/// One variant (dark or bright) of a theme.
|
||||
class Palette {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,12 @@ class StatBox extends StatelessWidget {
|
||||
builder: (context, constraints) {
|
||||
final baseSize = constraints.maxHeight * 0.4;
|
||||
|
||||
return Column(
|
||||
// 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,
|
||||
children: [
|
||||
// Value + optional unit
|
||||
@@ -62,6 +67,7 @@ class StatBox extends StatelessWidget {
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontFamily: hudFont,
|
||||
fontSize: baseSize,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
@@ -75,6 +81,7 @@ class StatBox extends StatelessWidget {
|
||||
Text(
|
||||
unit!,
|
||||
style: TextStyle(
|
||||
fontFamily: hudFont,
|
||||
fontSize: baseSize * 0.5,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: theme.subdued,
|
||||
@@ -89,6 +96,7 @@ class StatBox extends StatelessWidget {
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontFamily: hudFont,
|
||||
fontSize: baseSize * 0.6,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: theme.subdued,
|
||||
@@ -96,6 +104,8 @@ class StatBox extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -134,7 +144,12 @@ class StatBoxMain extends StatelessWidget {
|
||||
// Scale fonts relative to box height for consistent proportions
|
||||
final baseSize = constraints.maxHeight * 0.4;
|
||||
|
||||
return Column(
|
||||
// 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,
|
||||
children: [
|
||||
// Value + optional unit row
|
||||
@@ -146,6 +161,7 @@ class StatBoxMain extends StatelessWidget {
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontFamily: hudFont,
|
||||
fontSize: baseSize,
|
||||
fontWeight: FontWeight.w300,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
@@ -162,6 +178,7 @@ class StatBoxMain extends StatelessWidget {
|
||||
Text(
|
||||
unit!,
|
||||
style: TextStyle(
|
||||
fontFamily: hudFont,
|
||||
fontSize: baseSize * 0.4,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: theme.subdued,
|
||||
@@ -176,6 +193,7 @@ class StatBoxMain extends StatelessWidget {
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontFamily: hudFont,
|
||||
fontSize: baseSize * 0.35,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: theme.subdued,
|
||||
@@ -183,6 +201,8 @@ class StatBoxMain extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -57,6 +57,12 @@ flutter:
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
# "Alte DIN 1451 Mittelschrift" (Peter Wiegel), the HUD face of smart-serow and gps-speedo.
|
||||
fonts:
|
||||
- family: DIN1451
|
||||
fonts:
|
||||
- asset: assets/fonts/din1451alt.ttf
|
||||
|
||||
# Exact copies of contracts files (tool/sync-contracts.sh); crosscheck verifies them.
|
||||
assets:
|
||||
- assets/contracts/checklists/
|
||||
|
||||
Reference in New Issue
Block a user