// 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 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 load([String name = themeName]) async { for (final n in {name, 'default'}) { try { final j = jsonDecode( await rootBundle.loadString('assets/themes/$n.json'), ) as Map; return Themes( n, Palette._fromJson(j['dark'] as Map), Palette._fromJson(j['bright'] as Map), ); } 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(); assert(t != null, 'No AppTheme above this widget'); return t?.palette ?? Palette.fallbackDark; } @override bool updateShouldNotify(AppTheme old) => palette != old.palette; }