Files
app-flutter/lib/theme/palette.dart
T
mikkeli 8d95514897
/ crosscheck (push) Successful in 1s
CI / Analyze & Test (push) Successful in 14s
CI / Debug APK (push) Successful in 36s
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
2026-10-14 13:20:34 +09:00

148 lines
4.4 KiB
Dart

// The palette system, from smart-serow's HUD: four semantic colours, each with a dark
// (low ambient light) and a bright (high ambient light) variant, read from a theme
// JSON in assets/themes/. Same file format as smart-serow's extra/themes/, so themes
// move between the two as they are. Widgets ask for meanings, never for colours:
//
// final p = AppTheme.of(context);
// color: p.foreground
//
// 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';
/// 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 {
const Palette({
required this.background,
required this.foreground,
required this.highlight,
required this.subdued,
});
/// Behind everything.
final Color background;
/// Values: what is read at a glance.
final Color foreground;
/// Warnings, and the one thing that must be seen.
final Color highlight;
/// Labels, units, grids, and anything missing or stale.
final Color subdued;
static Palette _fromJson(Map<String, dynamic> j) {
Color c(String k) =>
Color(0xFF000000 | int.parse((j[k] as String).substring(1), radix: 16));
return Palette(
background: c('background'),
foreground: c('foreground'),
highlight: c('highlight'),
subdued: c('subdued'),
);
}
/// smart-serow's hard-coded last resort, used when no theme file loads.
static const fallbackDark = Palette(
background: Color(0xFF000000),
foreground: Color(0xFFFFFFFF),
highlight: Color(0xFFFF5555),
subdued: Color(0xFF808080),
);
static const fallbackBright = Palette(
background: Color(0xFFF0F0F0),
foreground: Color(0xFF1A1A1A),
highlight: Color(0xFFCC0000),
subdued: Color(0xFF606060),
);
}
/// A theme: both variants, and which one is showing. Listen to it to follow a switch.
class Themes extends ChangeNotifier {
Themes(this.name, this.dark, this.bright);
final String name;
final Palette dark;
final Palette bright;
bool _isDark = true;
bool get isDark => _isDark;
Palette get current => _isDark ? dark : bright;
set isDark(bool v) {
if (v == _isDark) return;
_isDark = v;
notifyListeners();
}
void toggle() => isDark = !_isDark;
/// [themeName], falling back to `default`, then to the hard-coded palette
/// (smart-serow's chain).
static Future<Themes> load([String name = themeName]) async {
for (final n in {name, 'default'}) {
try {
final j = jsonDecode(
await rootBundle.loadString('assets/themes/$n.json'),
) as Map<String, dynamic>;
return Themes(
n,
Palette._fromJson(j['dark'] as Map<String, dynamic>),
Palette._fromJson(j['bright'] as Map<String, dynamic>),
);
} catch (_) {
// Missing or malformed: try the next one.
}
}
return Themes('fallback', Palette.fallbackDark, Palette.fallbackBright);
}
/// Material's theme for the rest of the app, from the same four colours.
ThemeData material() {
final p = current;
final brightness = _isDark ? Brightness.dark : Brightness.light;
return ThemeData(
useMaterial3: true,
brightness: brightness,
scaffoldBackgroundColor: p.background,
colorScheme:
ColorScheme.fromSeed(
seedColor: p.highlight,
brightness: brightness,
).copyWith(
surface: p.background,
onSurface: p.foreground,
primary: p.highlight,
error: p.highlight,
outline: p.subdued,
onSurfaceVariant: p.subdued,
),
);
}
}
/// The current palette, for any widget below it: `AppTheme.of(context).foreground`.
class AppTheme extends InheritedWidget {
const AppTheme({super.key, required this.palette, required super.child});
final Palette palette;
static Palette of(BuildContext context) {
final t = context.dependOnInheritedWidgetOfExactType<AppTheme>();
assert(t != null, 'No AppTheme above this widget');
return t?.palette ?? Palette.fallbackDark;
}
@override
bool updateShouldNotify(AppTheme old) => palette != old.palette;
}