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 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'boundary.dart';
|
import 'boundary.dart';
|
||||||
|
import 'reading.dart';
|
||||||
import 'theme/palette.dart';
|
import 'theme/palette.dart';
|
||||||
import 'widgets/accel_graph.dart';
|
import 'widgets/accel_graph.dart';
|
||||||
|
import 'widgets/gps_compass.dart';
|
||||||
import 'widgets/stat_box.dart';
|
import 'widgets/stat_box.dart';
|
||||||
|
import 'widgets/telltale.dart';
|
||||||
import 'widgets/whiskey_mark.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 {
|
class DashPage extends StatelessWidget {
|
||||||
const DashPage({super.key, required this.boundary, required this.themes});
|
const DashPage({super.key, required this.boundary, required this.themes});
|
||||||
final Boundary boundary;
|
final Boundary boundary;
|
||||||
@@ -54,6 +33,13 @@ class DashPage extends StatelessWidget {
|
|||||||
final coolant = r('obd_coolant_temp_c');
|
final coolant = r('obd_coolant_temp_c');
|
||||||
final lat = r('imu_accel_lat_g');
|
final lat = r('imu_accel_lat_g');
|
||||||
final lon = r('imu_accel_lon_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(
|
return Scaffold(
|
||||||
backgroundColor: p.background,
|
backgroundColor: p.background,
|
||||||
@@ -139,6 +125,12 @@ class DashPage extends StatelessWidget {
|
|||||||
dim: coolant.dim,
|
dim: coolant.dim,
|
||||||
isWarning: () => (coolant.number ?? 0) > 105,
|
isWarning: () => (coolant.number ?? 0) > 105,
|
||||||
),
|
),
|
||||||
|
Expanded(
|
||||||
|
child: GpsCompass(
|
||||||
|
heading: r('gnss_heading_deg').number,
|
||||||
|
gpsState: gpsState,
|
||||||
|
),
|
||||||
|
),
|
||||||
Telltale(
|
Telltale(
|
||||||
label: 'LEFT',
|
label: 'LEFT',
|
||||||
icon: Icons.arrow_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.
|
/// The dash's own thin top line: link state, back, and the dark/bright switch.
|
||||||
class _DashBar extends StatelessWidget {
|
class _DashBar extends StatelessWidget {
|
||||||
const _DashBar({required this.boundary, required this.themes});
|
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);
|
// final p = AppTheme.of(context);
|
||||||
// color: p.foreground
|
// 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 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
const themeName = String.fromEnvironment(
|
/// The HUD face (smart-serow's DIN 1451). Opt-in: a widget names it in its own
|
||||||
'YUKART_THEME',
|
/// TextStyle; everything else keeps the basic font, which covers CJK where DIN does not.
|
||||||
defaultValue: 'default',
|
const hudFont = 'DIN1451';
|
||||||
);
|
|
||||||
|
const themeName = String.fromEnvironment('YUKART_THEME', defaultValue: 'rei');
|
||||||
|
|
||||||
/// One variant (dark or bright) of a theme.
|
/// One variant (dark or bright) of a theme.
|
||||||
class Palette {
|
class Palette {
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ class _AccelGraphState extends State<AccelGraph> {
|
|||||||
Text(
|
Text(
|
||||||
'Lon: ${_formatAccel(widget.ay)} (${_formatAccel(_ghostAy)})',
|
'Lon: ${_formatAccel(widget.ay)} (${_formatAccel(_ghostAy)})',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
fontFamily: hudFont,
|
||||||
fontSize: fontSize * 0.5,
|
fontSize: fontSize * 0.5,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
fontFeatures: const [FontFeature.tabularFigures()],
|
fontFeatures: const [FontFeature.tabularFigures()],
|
||||||
@@ -136,6 +137,7 @@ class _AccelGraphState extends State<AccelGraph> {
|
|||||||
Text(
|
Text(
|
||||||
'Lat: ${_formatAccel(widget.ax)} (${_formatAccel(_ghostAx)})',
|
'Lat: ${_formatAccel(widget.ax)} (${_formatAccel(_ghostAx)})',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
fontFamily: hudFont,
|
||||||
fontSize: fontSize * 0.5,
|
fontSize: fontSize * 0.5,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
fontFeatures: const [FontFeature.tabularFigures()],
|
fontFeatures: const [FontFeature.tabularFigures()],
|
||||||
@@ -149,6 +151,7 @@ class _AccelGraphState extends State<AccelGraph> {
|
|||||||
Text(
|
Text(
|
||||||
'Acceleration',
|
'Acceleration',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
fontFamily: hudFont,
|
||||||
fontSize: fontSize * 0.8,
|
fontSize: fontSize * 0.8,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
color: theme.subdued,
|
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) {
|
builder: (context, constraints) {
|
||||||
final baseSize = constraints.maxHeight * 0.4;
|
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,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Value + optional unit
|
// Value + optional unit
|
||||||
@@ -62,6 +67,7 @@ class StatBox extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
value,
|
value,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
fontFamily: hudFont,
|
||||||
fontSize: baseSize,
|
fontSize: baseSize,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
fontFeatures: const [FontFeature.tabularFigures()],
|
fontFeatures: const [FontFeature.tabularFigures()],
|
||||||
@@ -75,6 +81,7 @@ class StatBox extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
unit!,
|
unit!,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
fontFamily: hudFont,
|
||||||
fontSize: baseSize * 0.5,
|
fontSize: baseSize * 0.5,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
color: theme.subdued,
|
color: theme.subdued,
|
||||||
@@ -89,6 +96,7 @@ class StatBox extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
fontFamily: hudFont,
|
||||||
fontSize: baseSize * 0.6,
|
fontSize: baseSize * 0.6,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
color: theme.subdued,
|
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
|
// Scale fonts relative to box height for consistent proportions
|
||||||
final baseSize = constraints.maxHeight * 0.4;
|
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,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Value + optional unit row
|
// Value + optional unit row
|
||||||
@@ -146,6 +161,7 @@ class StatBoxMain extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
value,
|
value,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
fontFamily: hudFont,
|
||||||
fontSize: baseSize,
|
fontSize: baseSize,
|
||||||
fontWeight: FontWeight.w300,
|
fontWeight: FontWeight.w300,
|
||||||
fontFeatures: const [FontFeature.tabularFigures()],
|
fontFeatures: const [FontFeature.tabularFigures()],
|
||||||
@@ -162,6 +178,7 @@ class StatBoxMain extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
unit!,
|
unit!,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
fontFamily: hudFont,
|
||||||
fontSize: baseSize * 0.4,
|
fontSize: baseSize * 0.4,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
color: theme.subdued,
|
color: theme.subdued,
|
||||||
@@ -176,6 +193,7 @@ class StatBoxMain extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
fontFamily: hudFont,
|
||||||
fontSize: baseSize * 0.35,
|
fontSize: baseSize * 0.35,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
color: theme.subdued,
|
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(
|
Text(
|
||||||
'Roll: ${_formatAngle(roll)}',
|
'Roll: ${_formatAngle(roll)}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
fontFamily: hudFont,
|
||||||
fontSize: fontSize * 0.5,
|
fontSize: fontSize * 0.5,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
fontFeatures: const [FontFeature.tabularFigures()],
|
fontFeatures: const [FontFeature.tabularFigures()],
|
||||||
@@ -74,6 +75,7 @@ class WhiskeyMark extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
'Pitch: ${_formatAngle(pitch)}',
|
'Pitch: ${_formatAngle(pitch)}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
fontFamily: hudFont,
|
||||||
fontSize: fontSize * 0.5,
|
fontSize: fontSize * 0.5,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
fontFeatures: const [FontFeature.tabularFigures()],
|
fontFeatures: const [FontFeature.tabularFigures()],
|
||||||
@@ -87,6 +89,7 @@ class WhiskeyMark extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
'Attitude',
|
'Attitude',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
fontFamily: hudFont,
|
||||||
fontSize: fontSize * 0.8,
|
fontSize: fontSize * 0.8,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
color: theme.subdued,
|
color: theme.subdued,
|
||||||
|
|||||||
@@ -57,6 +57,12 @@ flutter:
|
|||||||
# the material Icons class.
|
# the material Icons class.
|
||||||
uses-material-design: true
|
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.
|
# Exact copies of contracts files (tool/sync-contracts.sh); crosscheck verifies them.
|
||||||
assets:
|
assets:
|
||||||
- assets/contracts/checklists/
|
- assets/contracts/checklists/
|
||||||
|
|||||||
Reference in New Issue
Block a user