diff --git a/lib/widgets/theme.dart b/lib/widgets/theme.dart index ac6ff28f13..c8742cae72 100644 --- a/lib/widgets/theme.dart +++ b/lib/widgets/theme.dart @@ -5,11 +5,50 @@ import 'content.dart'; import 'stream_colors.dart'; import 'text.dart'; +/// In debug mode, controls whether the UI responds to +/// [MediaQueryData.platformBrightness]. +/// +/// Outside of debug mode, this is always false and the setter has no effect. +// TODO(#95) when dark theme is fully implemented, simplify away; +// the UI should always respond. +bool get debugFollowPlatformBrightness { + bool result = false; + assert(() { + result = _debugFollowPlatformBrightness; + return true; + }()); + return result; +} +bool _debugFollowPlatformBrightness = false; +set debugFollowPlatformBrightness(bool value) { + assert(() { + _debugFollowPlatformBrightness = value; + return true; + }()); +} + + ThemeData zulipThemeData(BuildContext context) { - final designVariables = DesignVariables(); + final DesignVariables designVariables; + final List themeExtensions; + Brightness brightness = debugFollowPlatformBrightness + ? MediaQuery.of(context).platformBrightness + : Brightness.light; + switch (brightness) { + case Brightness.light: { + designVariables = DesignVariables.light(); + themeExtensions = [ContentTheme.light(context), designVariables]; + } + case Brightness.dark: { + designVariables = DesignVariables.dark(); + themeExtensions = [ContentTheme.dark(context), designVariables]; + } + } + return ThemeData( + brightness: brightness, typography: zulipTypography(context), - extensions: [ContentTheme.light(context), designVariables], + extensions: themeExtensions, appBarTheme: AppBarTheme( // Set these two fields to prevent a color change in [AppBar]s when // there is something scrolled under it. If an app bar hasn't been @@ -57,9 +96,10 @@ ThemeData zulipThemeData(BuildContext context) { // Or try this tool to see the whole palette: // https://m3.material.io/theme-builder#/custom colorScheme: ColorScheme.fromSeed( + brightness: brightness, seedColor: kZulipBrandColor, ), - scaffoldBackgroundColor: designVariables.bgMain, + scaffoldBackgroundColor: designVariables.mainBackground, tooltipTheme: const TooltipThemeData(preferBelow: false), ); } @@ -75,19 +115,31 @@ const kZulipBrandColor = Color.fromRGBO(0x64, 0x92, 0xfe, 1); /// For how to export these from the Figma, see: /// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=2945-49492&t=MEb4vtp7S26nntxm-0 class DesignVariables extends ThemeExtension { - DesignVariables() : - bgMain = const Color(0xfff0f0f0), - bgTopBar = const Color(0xfff5f5f5), - borderBar = const Color(0x33000000), - icon = const Color(0xff666699), - title = const Color(0xff1a1a1a), - streamColorSwatches = StreamColorSwatches.light; + DesignVariables.light() : + this._( + bgTopBar: const Color(0xfff5f5f5), + borderBar: const Color(0x33000000), + icon: const Color(0xff666699), + mainBackground: const Color(0xfff0f0f0), + title: const Color(0xff1a1a1a), + streamColorSwatches: StreamColorSwatches.light, + ); + + DesignVariables.dark() : + this._( + bgTopBar: const Color(0xff242424), + borderBar: Colors.black.withOpacity(0.41), + icon: const Color(0xff7070c2), + mainBackground: const Color(0xff1d1d1d), + title: const Color(0xffffffff), + streamColorSwatches: StreamColorSwatches.dark, + ); DesignVariables._({ - required this.bgMain, required this.bgTopBar, required this.borderBar, required this.icon, + required this.mainBackground, required this.title, required this.streamColorSwatches, }); @@ -102,10 +154,10 @@ class DesignVariables extends ThemeExtension { return extension!; } - final Color bgMain; final Color bgTopBar; final Color borderBar; final Color icon; + final Color mainBackground; final Color title; // Not exactly from the Figma design, but from Vlad anyway. @@ -113,18 +165,18 @@ class DesignVariables extends ThemeExtension { @override DesignVariables copyWith({ - Color? bgMain, Color? bgTopBar, Color? borderBar, Color? icon, + Color? mainBackground, Color? title, StreamColorSwatches? streamColorSwatches, }) { return DesignVariables._( - bgMain: bgMain ?? this.bgMain, bgTopBar: bgTopBar ?? this.bgTopBar, borderBar: borderBar ?? this.borderBar, icon: icon ?? this.icon, + mainBackground: mainBackground ?? this.mainBackground, title: title ?? this.title, streamColorSwatches: streamColorSwatches ?? this.streamColorSwatches, ); @@ -136,10 +188,10 @@ class DesignVariables extends ThemeExtension { return this; } return DesignVariables._( - bgMain: Color.lerp(bgMain, other.bgMain, t)!, bgTopBar: Color.lerp(bgTopBar, other.bgTopBar, t)!, borderBar: Color.lerp(borderBar, other.borderBar, t)!, icon: Color.lerp(icon, other.icon, t)!, + mainBackground: Color.lerp(mainBackground, other.mainBackground, t)!, title: Color.lerp(title, other.title, t)!, streamColorSwatches: StreamColorSwatches.lerp(streamColorSwatches, other.streamColorSwatches, t), ); diff --git a/test/widgets/theme_test.dart b/test/widgets/theme_test.dart index 763aab5d11..b6d3c98a68 100644 --- a/test/widgets/theme_test.dart +++ b/test/widgets/theme_test.dart @@ -76,30 +76,74 @@ void main() { button: TextButton(onPressed: () {}, child: const Text(buttonText))); }); - group('colorSwatchFor', () { - void doTest(String description, int baseColor, StreamColorSwatch expected) { - testWidgets('$description $baseColor', (WidgetTester tester) async { - addTearDown(testBinding.reset); - - final subscription = eg.subscription(eg.stream(), color: baseColor); + group('DesignVariables', () { + group('lerp', () { + testWidgets('light -> light', (tester) async { + final a = DesignVariables.light(); + final b = DesignVariables.light(); + check(() => a.lerp(b, 0.5)).returnsNormally(); + }); - await tester.pumpWidget(const ZulipApp()); - await tester.pump(); + testWidgets('light -> dark', (tester) async { + final a = DesignVariables.light(); + final b = DesignVariables.dark(); + check(() => a.lerp(b, 0.5)).returnsNormally(); + }); - late StreamColorSwatch actualSwatch; - final navigator = await ZulipApp.navigator; - navigator.push(MaterialWidgetRoute(page: Builder(builder: (context) { - actualSwatch = colorSwatchFor(context, subscription); - return const Placeholder(); - }))); - await tester.pumpAndSettle(); + testWidgets('dark -> light', (tester) async { + final a = DesignVariables.dark(); + final b = DesignVariables.light(); + check(() => a.lerp(b, 0.5)).returnsNormally(); + }); - // Compares all the swatch's members; see [ColorSwatch]'s `operator ==`. - check(actualSwatch).equals(expected); + testWidgets('dark -> dark', (tester) async { + final a = DesignVariables.dark(); + final b = DesignVariables.dark(); + check(() => a.lerp(b, 0.5)).returnsNormally(); }); - } + }); + }); - doTest('light', 0xff76ce90, StreamColorSwatch.light(0xff76ce90)); - // TODO(#95) test with Brightness.dark and lerping between light/dark + group('colorSwatchFor', () { + const baseColor = 0xff76ce90; + + testWidgets('light–dark animation', (WidgetTester tester) async { + addTearDown(testBinding.reset); + + final subscription = eg.subscription(eg.stream(), color: baseColor); + + assert(!debugFollowPlatformBrightness); // to be removed with #95 + debugFollowPlatformBrightness = true; + addTearDown(() { debugFollowPlatformBrightness = false; }); + tester.platformDispatcher.platformBrightnessTestValue = Brightness.light; + addTearDown(tester.platformDispatcher.clearPlatformBrightnessTestValue); + + await tester.pumpWidget(const ZulipApp()); + await tester.pump(); + + final navigator = await ZulipApp.navigator; + navigator.push(MaterialWidgetRoute(page: Builder(builder: (context) => + const Placeholder()))); + await tester.pumpAndSettle(); + + final element = tester.element(find.byType(Placeholder)); + // Compares all the swatch's members; see [ColorSwatch]'s `operator ==`. + check(colorSwatchFor(element, subscription)) + .equals(StreamColorSwatch.light(baseColor)); + + tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark; + await tester.pump(); + + await tester.pump(kThemeAnimationDuration * 0.4); + check(colorSwatchFor(element, subscription)) + .equals(StreamColorSwatch.lerp( + StreamColorSwatch.light(baseColor), + StreamColorSwatch.dark(baseColor), + 0.4)!); + + await tester.pump(kThemeAnimationDuration * 0.6); + check(colorSwatchFor(element, subscription)) + .equals(StreamColorSwatch.dark(baseColor)); + }); }); }