7c9627fffb
Changelog: Optional two-tier Liquid Glass and slide-to-pick menus
270 lines
7.9 KiB
Dart
270 lines
7.9 KiB
Dart
import 'package:fl_clash/ui/color.dart';
|
|
import 'package:material_ui/material_ui.dart';
|
|
|
|
/// `.agents/surfaces.md` explains the levels and why dark mode never steps
|
|
/// back down.
|
|
enum SurfaceLevel {
|
|
page,
|
|
card,
|
|
block,
|
|
inset;
|
|
|
|
static SurfaceLevel floatingOf(ColorScheme colorScheme) =>
|
|
colorScheme.brightness == Brightness.dark ? block : card;
|
|
|
|
SurfaceLevel get next => switch (this) {
|
|
page => card,
|
|
card => block,
|
|
block || inset => inset,
|
|
};
|
|
|
|
Color colorOf(ColorScheme colorScheme) {
|
|
final dark = colorScheme.brightness == Brightness.dark;
|
|
return switch (this) {
|
|
page => dark ? colorScheme.surface : colorScheme.surfaceContainer,
|
|
card => dark ? colorScheme.surfaceContainer : colorScheme.surfaceBright,
|
|
block =>
|
|
dark ? colorScheme.surfaceContainerHigh : colorScheme.surfaceContainer,
|
|
inset => colorScheme.surfaceContainerHighest,
|
|
};
|
|
}
|
|
}
|
|
|
|
class SurfaceScope extends StatelessWidget {
|
|
const SurfaceScope({super.key, required this.level, required this.child});
|
|
|
|
final SurfaceLevel level;
|
|
final Widget child;
|
|
|
|
static SurfaceLevel of(BuildContext context) =>
|
|
context.dependOnInheritedWidgetOfExactType<_SurfaceLevelScope>()?.level ??
|
|
SurfaceLevel.page;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return _SurfaceLevelScope(level: level, child: child);
|
|
}
|
|
}
|
|
|
|
class _SurfaceLevelScope extends InheritedWidget {
|
|
const _SurfaceLevelScope({required this.level, required super.child});
|
|
|
|
final SurfaceLevel level;
|
|
|
|
@override
|
|
bool updateShouldNotify(_SurfaceLevelScope oldWidget) =>
|
|
level != oldWidget.level;
|
|
}
|
|
|
|
extension SurfaceContext on BuildContext {
|
|
SurfaceLevel get surfaceLevel => SurfaceScope.of(this);
|
|
|
|
SurfaceLevel get raisedSurfaceLevel => surfaceLevel.next;
|
|
}
|
|
|
|
extension ChromeSurfaceExt on ColorScheme {
|
|
Color get chromeSurface => brightness == Brightness.dark
|
|
? surfaceContainerLow
|
|
: surfaceContainerHighest;
|
|
}
|
|
|
|
extension ElevatedSurfaceExt on ColorScheme {
|
|
ColorScheme get elevated {
|
|
if (brightness != Brightness.dark) {
|
|
return this;
|
|
}
|
|
final lift =
|
|
SurfaceLevel.card.colorOf(this).tone -
|
|
SurfaceLevel.page.colorOf(this).tone;
|
|
Color raise(Color color) => color.shiftTone(lift);
|
|
return copyWith(
|
|
surface: raise(surface),
|
|
surfaceDim: raise(surfaceDim),
|
|
surfaceBright: raise(surfaceBright),
|
|
surfaceContainerLowest: raise(surfaceContainerLowest),
|
|
surfaceContainerLow: raise(surfaceContainerLow),
|
|
surfaceContainer: raise(surfaceContainer),
|
|
surfaceContainerHigh: raise(surfaceContainerHigh),
|
|
surfaceContainerHighest: raise(surfaceContainerHighest),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// The scheme sheets present with; `.agents/surfaces.md` has the reason.
|
|
class ElevatedColorScheme extends ThemeExtension<ElevatedColorScheme> {
|
|
const ElevatedColorScheme(this.colorScheme);
|
|
|
|
final ColorScheme colorScheme;
|
|
|
|
@override
|
|
ElevatedColorScheme copyWith({ColorScheme? colorScheme}) =>
|
|
ElevatedColorScheme(colorScheme ?? this.colorScheme);
|
|
|
|
@override
|
|
ElevatedColorScheme lerp(ElevatedColorScheme? other, double t) =>
|
|
other == null
|
|
? this
|
|
: ElevatedColorScheme(
|
|
ColorScheme.lerp(colorScheme, other.colorScheme, t),
|
|
);
|
|
}
|
|
|
|
class ElevatedSurfaceTheme extends StatelessWidget {
|
|
const ElevatedSurfaceTheme({super.key, required this.child});
|
|
|
|
final Widget child;
|
|
|
|
static bool isIn(BuildContext context) =>
|
|
context.getInheritedWidgetOfExactType<_ElevatedSurfaceScope>() != null;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return _ElevatedSurfaceScope(
|
|
child: Theme(data: Theme.of(context).elevated, child: child),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ElevatedSurfaceScope extends InheritedWidget {
|
|
const _ElevatedSurfaceScope({required super.child});
|
|
|
|
@override
|
|
bool updateShouldNotify(_ElevatedSurfaceScope oldWidget) => false;
|
|
}
|
|
|
|
extension AppSurfaceThemeExt on ThemeData {
|
|
ThemeData get elevated {
|
|
final scheme = extension<ElevatedColorScheme>()?.colorScheme;
|
|
if (scheme == null) {
|
|
return this;
|
|
}
|
|
return copyWith(
|
|
colorScheme: scheme,
|
|
canvasColor: scheme.surface,
|
|
).withAppSurfaces;
|
|
}
|
|
|
|
ThemeData get withAppSurfaces {
|
|
final page = SurfaceLevel.page.colorOf(colorScheme);
|
|
final card = SurfaceLevel.card.colorOf(colorScheme);
|
|
final floatingLevel = SurfaceLevel.floatingOf(colorScheme);
|
|
final floating = floatingLevel.colorOf(colorScheme);
|
|
final ring = focusRingStyle(colorScheme);
|
|
final standingRing = focusRingStyle(colorScheme, standsOff: true);
|
|
return copyWith(
|
|
dividerTheme: dividerTheme.copyWith(
|
|
color: colorScheme.onSurface.withValues(alpha: 0.14),
|
|
),
|
|
scaffoldBackgroundColor: page,
|
|
cardTheme: cardTheme.copyWith(color: card, elevation: 0),
|
|
dialogTheme: dialogTheme.copyWith(backgroundColor: floating),
|
|
popupMenuTheme: popupMenuTheme.copyWith(color: floating),
|
|
menuTheme: MenuThemeData(
|
|
style: (menuTheme.style ?? const MenuStyle()).copyWith(
|
|
backgroundColor: WidgetStatePropertyAll(floating),
|
|
),
|
|
),
|
|
navigationBarTheme: navigationBarTheme.copyWith(
|
|
backgroundColor: floating,
|
|
indicatorColor: floatingLevel.next.colorOf(colorScheme),
|
|
iconTheme: WidgetStateProperty.resolveWith(
|
|
(states) => IconThemeData(
|
|
size: 24,
|
|
color: states.contains(WidgetState.selected)
|
|
? colorScheme.primary
|
|
: colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
bottomSheetTheme: bottomSheetTheme.copyWith(
|
|
backgroundColor: page,
|
|
modalBackgroundColor: page,
|
|
),
|
|
filledButtonTheme: FilledButtonThemeData(
|
|
style: standingRing.merge(filledButtonTheme.style),
|
|
),
|
|
textButtonTheme: TextButtonThemeData(
|
|
style: ring.merge(textButtonTheme.style),
|
|
),
|
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
style: ring.merge(outlinedButtonTheme.style),
|
|
),
|
|
iconButtonTheme: IconButtonThemeData(
|
|
style: ring.merge(iconButtonTheme.style),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
const _focusRingWidth = 2.0;
|
|
const _focusRingGap = 1.0;
|
|
|
|
/// The focus ring of `.agents/surfaces.md`; one that [standsOff] clears the
|
|
/// edge, which a primary fill or a button's own glass would hide it on.
|
|
BorderSide focusRingSide(ColorScheme colorScheme, {bool standsOff = false}) =>
|
|
BorderSide(
|
|
color: colorScheme.primary,
|
|
width: _focusRingWidth,
|
|
strokeAlign: standsOff
|
|
? 1 + 2 * _focusRingGap / _focusRingWidth
|
|
: BorderSide.strokeAlignInside,
|
|
);
|
|
|
|
ButtonStyle focusRingStyle(ColorScheme colorScheme, {bool standsOff = false}) =>
|
|
ButtonStyle(
|
|
side: WidgetStateProperty<BorderSide?>.fromMap({
|
|
WidgetState.focused: focusRingSide(colorScheme, standsOff: standsOff),
|
|
}),
|
|
);
|
|
|
|
/// [focusRingSide] on [shape] while focus is in [child], for ink not a button.
|
|
class FocusRing extends StatefulWidget {
|
|
const FocusRing({
|
|
super.key,
|
|
required this.shape,
|
|
this.standsOff = false,
|
|
required this.child,
|
|
});
|
|
|
|
final OutlinedBorder shape;
|
|
final bool standsOff;
|
|
final Widget child;
|
|
|
|
@override
|
|
State<FocusRing> createState() => _FocusRingState();
|
|
}
|
|
|
|
class _FocusRingState extends State<FocusRing> {
|
|
var _focused = false;
|
|
|
|
void _handleFocusChange(bool focused) {
|
|
if (focused != _focused) {
|
|
setState(() => _focused = focused);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Focus(
|
|
canRequestFocus: false,
|
|
skipTraversal: true,
|
|
includeSemantics: false,
|
|
onFocusChange: _handleFocusChange,
|
|
child: DecoratedBox(
|
|
position: DecorationPosition.foreground,
|
|
decoration: ShapeDecoration(
|
|
shape: widget.shape.copyWith(
|
|
side: _focused
|
|
? focusRingSide(
|
|
Theme.of(context).colorScheme,
|
|
standsOff: widget.standsOff,
|
|
)
|
|
: BorderSide.none,
|
|
),
|
|
),
|
|
child: widget.child,
|
|
),
|
|
);
|
|
}
|
|
}
|