diff --git a/lib/widgets/app.dart b/lib/widgets/app.dart index 3ff1eee459..281e0ffbee 100644 --- a/lib/widgets/app.dart +++ b/lib/widgets/app.dart @@ -15,6 +15,7 @@ import 'page.dart'; import 'recent_dm_conversations.dart'; import 'store.dart'; import 'subscription_list.dart'; +import 'text.dart'; class ZulipApp extends StatelessWidget { const ZulipApp({super.key, this.navigatorObservers}); @@ -81,6 +82,7 @@ class ZulipApp extends StatelessWidget { @override Widget build(BuildContext context) { final theme = ThemeData( + typography: zulipTypography(context), appBarTheme: const AppBarTheme( // This prevents an elevation change in [AppBar]s so they stop turning // darker if there is something scrolled underneath it. See docs: diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart index 1954b3931f..b569f43b55 100644 --- a/lib/widgets/content.dart +++ b/lib/widgets/content.dart @@ -100,11 +100,10 @@ class Paragraph extends StatelessWidget { final ParagraphNode node; - static TextStyle getTextStyle(BuildContext context) => const TextStyle( - fontFamily: 'Source Sans 3', - fontSize: 14, - height: (17 / 14), - ).merge(weightVariableTextStyle(context)); + static const textStyle = TextStyle( + fontSize: kBaseFontSize, + height: (17 / kBaseFontSize), + ); @override Widget build(BuildContext context) { @@ -114,7 +113,7 @@ class Paragraph extends StatelessWidget { final text = _buildBlockInlineContainer( node: node, - style: getTextStyle(context), + style: textStyle, ); // If the paragraph didn't actually have a `p` element in the HTML, @@ -647,7 +646,7 @@ class UserMention extends StatelessWidget { // One hopes an @-mention can't contain an embedded link. // (The parser on creating a UserMentionNode has a TODO to check that.) linkRecognizers: null, - style: Paragraph.getTextStyle(context), + style: Paragraph.textStyle, nodes: node.nodes)); } diff --git a/lib/widgets/emoji_reaction.dart b/lib/widgets/emoji_reaction.dart index 7487140367..12aed292e7 100644 --- a/lib/widgets/emoji_reaction.dart +++ b/lib/widgets/emoji_reaction.dart @@ -173,13 +173,11 @@ class ReactionChip extends StatelessWidget { textWidthBasis: TextWidthBasis.longestLine, textScaler: _labelTextScalerClamped(context), style: TextStyle( - fontFamily: 'Source Sans 3', fontSize: (14 * 0.90), height: 13 / (14 * 0.90), color: labelColor, - ).merge(selfVoted - ? weightVariableTextStyle(context, wght: 600, wghtIfPlatformRequestsBold: 900) - : weightVariableTextStyle(context)), + ).merge(weightVariableTextStyle(context, + wght: selfVoted ? 600 : null)), label), )), ]); @@ -352,13 +350,11 @@ class _TextEmoji extends StatelessWidget { textScaler: _textEmojiScalerClamped(context), textWidthBasis: TextWidthBasis.longestLine, style: TextStyle( - fontFamily: 'Source Sans 3', fontSize: 14 * 0.8, height: 1, // to be denser when we have to wrap color: selected ? _textColorSelected : _textColorUnselected, - ).merge(selected - ? weightVariableTextStyle(context, wght: 600, wghtIfPlatformRequestsBold: 900) - : weightVariableTextStyle(context)), + ).merge(weightVariableTextStyle(context, + wght: selected ? 600 : null)), // Encourage line breaks before "_" (common in these), but try not // to leave a colon alone on a line. See: // diff --git a/lib/widgets/inbox.dart b/lib/widgets/inbox.dart index e8e343aa91..efd28c042b 100644 --- a/lib/widgets/inbox.dart +++ b/lib/widgets/inbox.dart @@ -238,11 +238,10 @@ abstract class _HeaderItem extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 4), child: Text( style: const TextStyle( - fontFamily: 'Source Sans 3', fontSize: 17, height: (20 / 17), color: Color(0xFF222222), - ).merge(weightVariableTextStyle(context, wght: 600, wghtIfPlatformRequestsBold: 900)), + ).merge(weightVariableTextStyle(context, wght: 600)), maxLines: 1, overflow: TextOverflow.ellipsis, title))), @@ -359,11 +358,10 @@ class _DmItem extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 4), child: Text( style: const TextStyle( - fontFamily: 'Source Sans 3', fontSize: 17, height: (20 / 17), color: Color(0xFF222222), - ).merge(weightVariableTextStyle(context)), + ), maxLines: 2, overflow: TextOverflow.ellipsis, title))), @@ -485,11 +483,10 @@ class _TopicItem extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 4), child: Text( style: const TextStyle( - fontFamily: 'Source Sans 3', fontSize: 17, height: (20 / 17), color: Color(0xFF222222), - ).merge(weightVariableTextStyle(context)), + ), maxLines: 2, overflow: TextOverflow.ellipsis, topic))), diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 7358749fdc..625dd393c2 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -449,11 +449,15 @@ class MarkAsReadWidget extends StatelessWidget { style: FilledButton.styleFrom( backgroundColor: _UnreadMarker.color, minimumSize: const Size.fromHeight(38), - textStyle: const TextStyle( - fontFamily: 'Source Sans 3', - fontSize: 18, - height: (23 / 18), - ).merge(weightVariableTextStyle(context)), + textStyle: + // Restate [FilledButton]'s default, which inherits from + // [zulipTypography]… + Theme.of(context).textTheme.labelLarge! + // …then clobber some attributes to follow Figma: + .merge(const TextStyle( + fontSize: 18, + height: (23 / 18)) + .merge(weightVariableTextStyle(context, wght: 400))), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(7)), ), onPressed: () => _handlePress(context), @@ -631,11 +635,10 @@ class StreamMessageRecipientHeader extends StatelessWidget { } final textStyle = TextStyle( color: contrastingColor, - fontFamily: 'Source Sans 3', fontSize: 16, letterSpacing: 0.02 * 16, height: (18 / 16), - ).merge(weightVariableTextStyle(context, wght: 600, wghtIfPlatformRequestsBold: 900)); + ).merge(weightVariableTextStyle(context, wght: 600)); final Widget streamWidget; if (!showStream) { @@ -742,11 +745,10 @@ class DmRecipientHeader extends StatelessWidget { Expanded( child: Text(title, style: const TextStyle( - fontFamily: 'Source Sans 3', fontSize: 16, letterSpacing: 0.02 * 16, height: (18 / 16), - ).merge(weightVariableTextStyle(context, wght: 600, wghtIfPlatformRequestsBold: 900)), + ).merge(weightVariableTextStyle(context, wght: 600)), overflow: TextOverflow.ellipsis)), RecipientHeaderDate(message: message, color: _kDmRecipientHeaderDateColor), @@ -801,13 +803,12 @@ class DateText extends StatelessWidget { return Text( style: TextStyle( color: color, - fontFamily: 'Source Sans 3', fontSize: fontSize, height: height, // This is equivalent to css `all-small-caps`, see: // https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant-caps#all-small-caps fontFeatures: const [FontFeature.enable('c2sc'), FontFeature.enable('smcp')], - ).merge(weightVariableTextStyle(context)), + ), formatHeaderDate( zulipLocalizations, DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), diff --git a/lib/widgets/recent_dm_conversations.dart b/lib/widgets/recent_dm_conversations.dart index f8a84bf7b9..4791bd4b5c 100644 --- a/lib/widgets/recent_dm_conversations.dart +++ b/lib/widgets/recent_dm_conversations.dart @@ -8,7 +8,6 @@ import 'icons.dart'; import 'message_list.dart'; import 'page.dart'; import 'store.dart'; -import 'text.dart'; import 'unread_count_badge.dart'; class RecentDmConversationsPage extends StatefulWidget { @@ -127,11 +126,10 @@ class RecentDmConversationsItem extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 4), child: Text( style: const TextStyle( - fontFamily: 'Source Sans 3', fontSize: 17, height: (20 / 17), color: Color(0xFF222222), - ).merge(weightVariableTextStyle(context)), + ), maxLines: 2, overflow: TextOverflow.ellipsis, title))), diff --git a/lib/widgets/subscription_list.dart b/lib/widgets/subscription_list.dart index 935da57280..97c75f8eca 100644 --- a/lib/widgets/subscription_list.dart +++ b/lib/widgets/subscription_list.dart @@ -119,10 +119,9 @@ class _NoSubscriptionsItem extends StatelessWidget { textAlign: TextAlign.center, style: TextStyle( color: const HSLColor.fromAHSL(1.0, 240, 0.1, 0.5).toColor(), - fontFamily: 'Source Sans 3', fontSize: 18, height: (20 / 18), - ).merge(weightVariableTextStyle(context))))); + )))); } } @@ -148,11 +147,10 @@ class _SubscriptionListHeader extends StatelessWidget { textAlign: TextAlign.center, style: TextStyle( color: const HSLColor.fromAHSL(1.0, 240, 0.1, 0.5).toColor(), - fontFamily: 'Source Sans 3', fontSize: 14, letterSpacing: 0.04 * 14, height: (16 / 14), - ).merge(weightVariableTextStyle(context)))), + ))), const SizedBox(width: 8), Expanded(child: Divider( color: const HSLColor.fromAHSL(0.2, 240, 0.1, 0.5).toColor())), @@ -221,13 +219,11 @@ class SubscriptionItem extends StatelessWidget { // https://github.com/zulip/zulip-flutter/pull/397#pullrequestreview-1742524205 child: Text( style: const TextStyle( - fontFamily: 'Source Sans 3', fontSize: 18, height: (20 / 18), color: Color(0xFF262626), - ).merge(hasUnreads - ? weightVariableTextStyle(context, wght: 600, wghtIfPlatformRequestsBold: 900) - : weightVariableTextStyle(context)), + ).merge(weightVariableTextStyle(context, + wght: hasUnreads ? 600 : null)), maxLines: 1, overflow: TextOverflow.ellipsis, subscription.name))), diff --git a/lib/widgets/text.dart b/lib/widgets/text.dart index 1a80cee4bc..4fb9e1cec7 100644 --- a/lib/widgets/text.dart +++ b/lib/widgets/text.dart @@ -1,5 +1,139 @@ import 'dart:io'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// An app-wide [Typography] for Zulip, customized from the Material default. +/// +/// Include this in the app-wide [MaterialApp.theme]. +/// +/// We expect these text styles to be the basis of all the styles chosen by the +/// Material library's widgets, such as the default styling of +/// an [AppBar]'s title, of an [ElevatedButton]'s label, and so on. +/// +/// As of writing, it turns out that these styles also flow naturally into +/// most of our own widgets' text styles. +/// We often see this in the child of a [Material], for example, +/// since by default [Material] applies an [AnimatedDefaultTextStyle] +/// with the [TextTheme.bodyMedium] that gets its value from here. +/// +/// Applies [kDefaultFontFamily] and [kDefaultFontFamilyFallback], +/// being faithful to the Material-default font weights +/// by running them through [weightVariableTextStyle]. +/// (That is needed because [kDefaultFontFamily] is a variable-weight font). +/// +/// When building on top of these [TextStyles], callers that wish to specify +/// a different font weight are still responsible for reprocessing the style +/// with [weightVariableTextStyle] before passing it to a [Text]. +/// (Widgets in the Material library won't do this; they aren't yet equipped +/// to set font weights on variable-weight fonts. If this causes visible bugs, +/// we should investigate and fix, but such bugs should become less likely as +/// we transition from Material's widgets to our own bespoke ones.) +// TODO decide if we like this data flow for our own widgets' text styles. +// Does our design fit well with the fields of a [TextTheme]? +// (That's [TextTheme.titleLarge], [TextTheme.bodyMedium], etc.) +Typography zulipTypography(BuildContext context) { + final typography = Theme.of(context).typography; + + Typography result = typography.copyWith( + black: typography.black.apply( + fontFamily: kDefaultFontFamily, + fontFamilyFallback: defaultFontFamilyFallback), + white: typography.white.apply( + fontFamily: kDefaultFontFamily, + fontFamilyFallback: defaultFontFamilyFallback), + + dense: _weightVariableTextTheme(context, typography.dense), + englishLike: _weightVariableTextTheme(context, typography.englishLike), + tall: _weightVariableTextTheme(context, typography.tall), + ); + + assert(() { + // Set [TextStyle.debugLabel] for all styles, like: + // "zulipTypography black titleMedium" + + mkAddLabel(String debugTextThemeLabel) + => (TextStyle? maybeInputStyle, String debugStyleLabel) + => maybeInputStyle?.copyWith(debugLabel: '$debugTextThemeLabel $debugStyleLabel'); + + result = result.copyWith( + black: _convertTextTheme(result.black, mkAddLabel('zulipTypography black')), + white: _convertTextTheme(result.white, mkAddLabel('zulipTypography white')), + englishLike: _convertTextTheme(result.englishLike, mkAddLabel('zulipTypography englishLike')), + dense: _convertTextTheme(result.dense, mkAddLabel('zulipTypography dense')), + tall: _convertTextTheme(result.tall, mkAddLabel('zulipTypography tall')), + ); + return true; + }()); + + return result; +} + +/// Convert a geometry [TextTheme] to one that works with "wght"-variable fonts. +/// +/// A "geometry [TextTheme]" is a [TextTheme] that's meant to specify +/// font weight and other parameters about shape, size, distance, etc. +/// See [Typography]. +/// +/// This looks at each of the [TextStyle]s found on the input [TextTheme] +/// (such as [TextTheme.bodyMedium]), +/// and uses [weightVariableTextStyle] to adjust the [TextStyle]. +/// Fields that are null in the input [TextTheme] remain null in the output. +/// +/// For each input [TextStyle], the `wght` value passed +/// to [weightVariableTextStyle] is based on the input's [TextStyle.fontWeight]. +/// A null [TextStyle.fontWeight] is interpreted as the normal font weight. +TextTheme _weightVariableTextTheme(BuildContext context, TextTheme input) { + TextStyle? convert(TextStyle? maybeInputStyle, _) { + if (maybeInputStyle == null) { + return null; + } + final inputFontWeight = maybeInputStyle.fontWeight; + return maybeInputStyle.merge(weightVariableTextStyle(context, + wght: inputFontWeight != null + ? wghtFromFontWeight(inputFontWeight) + : null)); + } + + return _convertTextTheme(input, convert); +} + +TextTheme _convertTextTheme( + TextTheme input, + TextStyle? Function(TextStyle?, String debugStyleLabel) converter, +) => TextTheme( + displayLarge: converter(input.displayLarge, 'displayLarge'), + displayMedium: converter(input.displayMedium, 'displayMedium'), + displaySmall: converter(input.displaySmall, 'displaySmall'), + headlineLarge: converter(input.headlineLarge, 'headlineLarge'), + headlineMedium: converter(input.headlineMedium, 'headlineMedium'), + headlineSmall: converter(input.headlineSmall, 'headlineSmall'), + titleLarge: converter(input.titleLarge, 'titleLarge'), + titleMedium: converter(input.titleMedium, 'titleMedium'), + titleSmall: converter(input.titleSmall, 'titleSmall'), + bodyLarge: converter(input.bodyLarge, 'bodyLarge'), + bodyMedium: converter(input.bodyMedium, 'bodyMedium'), + bodySmall: converter(input.bodySmall, 'bodySmall'), + labelLarge: converter(input.labelLarge, 'labelLarge'), + labelMedium: converter(input.labelMedium, 'labelMedium'), + labelSmall: converter(input.labelSmall, 'labelSmall'), +); + +/// The [TextStyle.fontFamily] to use in most of the app. +/// +/// The same [TextStyle] should also specify [defaultFontFamilyFallback] +/// for [TextStyle.fontFamilyFallback]. +/// +/// This is a variable-weight font, so any [TextStyle] that uses this should be +/// merged with the result of calling [weightVariableTextStyle]. +const kDefaultFontFamily = 'Source Sans 3'; + +/// The [TextStyle.fontFamilyFallback] for use with [kDefaultFontFamily]. +List get defaultFontFamilyFallback => [ + // iOS doesn't support any of the formats this font is available in. + // If we use it on iOS, we'll get blank spaces where we could have had Apple- + // style emojis. + if (defaultTargetPlatform == TargetPlatform.android) 'Noto Color Emoji', +]; /// A mergeable [TextStyle] with 'Source Code Pro' and platform-aware fallbacks. /// @@ -52,18 +186,17 @@ TextStyle weightVariableTextStyle(BuildContext? context, { double? wght, double? wghtIfPlatformRequestsBold, }) { - assert((wght != null) == (wghtIfPlatformRequestsBold != null)); double value = wght ?? FontWeight.normal.value.toDouble(); - if (context != null && MediaQuery.of(context).boldText) { + if (context != null && MediaQuery.boldTextOf(context)) { // The framework has a condition on [MediaQueryData.boldText] // in the [Text] widget, but that only affects `fontWeight`. // [Text] doesn't know where to land on the chosen font's "wght" axis if any, // and indeed it doesn't seem updated to be aware of variable fonts at all. - value = wghtIfPlatformRequestsBold ?? FontWeight.bold.value.toDouble(); + value = wghtIfPlatformRequestsBold ?? bolderWght(value); } - assert(value >= 1 && value <= 1000); // https://fonts.google.com/variablefonts#axis-definitions + assert(value >= kWghtMin && value <= kWghtMax); - return TextStyle( + TextStyle result = TextStyle( fontVariations: [FontVariation('wght', value)], // This use of `fontWeight` shouldn't affect glyphs in the preferred, @@ -72,6 +205,34 @@ TextStyle weightVariableTextStyle(BuildContext? context, { fontWeight: clampVariableFontWeight(value), inherit: true); + + assert(() { + final attributes = [ + if (wght != null) 'wght: $wght', + if (wghtIfPlatformRequestsBold != null) 'wghtIfPlatformRequestsBold: $wghtIfPlatformRequestsBold', + ]; + result = result.copyWith( + debugLabel: 'weightVariableTextStyle(${attributes.join(', ')})'); + return true; + }()); + + return result; +} + +/// The minimum that a [FontVariation] "wght" value can be. +/// +/// See . +const kWghtMin = 1.0; + +/// The maximum that a [FontVariation] "wght" value can be. +/// +/// See . +const kWghtMax = 1000.0; + +/// A [FontVariation] "wght" value that's 300 above a given, clamped to [kWghtMax]. +double bolderWght(double baseWght) { + assert(kWghtMin <= baseWght && baseWght <= kWghtMax); + return clampDouble(baseWght + 300, kWghtMin, kWghtMax); } /// Find the nearest [FontWeight] constant for a variable-font "wght"-axis value. @@ -100,3 +261,12 @@ FontWeight clampVariableFontWeight(double wght) { } } } + +/// A good guess at a font's "wght" value to match a given [FontWeight]. +/// +/// Returns [FontWeight.value] as a double. +/// +/// This might not be exactly where the font designer would land on their +/// font's own custom-defined "wght" axis. But it's a great guess, +/// at least without knowledge of the particular font. +double wghtFromFontWeight(FontWeight fontWeight) => fontWeight.value.toDouble(); diff --git a/lib/widgets/unread_count_badge.dart b/lib/widgets/unread_count_badge.dart index dd7d97127f..45df67e80d 100644 --- a/lib/widgets/unread_count_badge.dart +++ b/lib/widgets/unread_count_badge.dart @@ -44,14 +44,12 @@ class UnreadCountBadge extends StatelessWidget { padding: const EdgeInsets.fromLTRB(4, 0, 4, 1), child: Text( style: const TextStyle( - fontFamily: 'Source Sans 3', fontSize: 16, height: (18 / 16), fontFeatures: [FontFeature.enable('smcp')], // small caps color: Color(0xFF222222), - ).merge(bold - ? weightVariableTextStyle(context, wght: 600, wghtIfPlatformRequestsBold: 900) - : weightVariableTextStyle(context)), + ).merge(weightVariableTextStyle(context, + wght: bold ? 600 : null)), count.toString()))); } } diff --git a/test/flutter_checks.dart b/test/flutter_checks.dart index 6fcd4a1a18..81977e9933 100644 --- a/test/flutter_checks.dart +++ b/test/flutter_checks.dart @@ -54,8 +54,37 @@ extension TextFieldChecks on Subject { extension TextStyleChecks on Subject { Subject get inherit => has((t) => t.inherit, 'inherit'); - Subject?> get fontVariations => has((t) => t.fontVariations, 'fontVariations'); Subject get fontWeight => has((t) => t.fontWeight, 'fontWeight'); + Subject?> get fontVariations => has((t) => t.fontVariations, 'fontVariations'); + Subject get fontFamily => has((t) => t.fontFamily, 'fontFamily'); + Subject?> get fontFamilyFallback => has((t) => t.fontFamilyFallback, 'fontFamilyFallback'); // TODO others } + + +extension TextThemeChecks on Subject { + Subject get displayLarge => has((t) => t.displayLarge, 'displayLarge'); + Subject get displayMedium => has((t) => t.displayMedium, 'displayMedium'); + Subject get displaySmall => has((t) => t.displaySmall, 'displaySmall'); + Subject get headlineLarge => has((t) => t.headlineLarge, 'headlineLarge'); + Subject get headlineMedium => has((t) => t.headlineMedium, 'headlineMedium'); + Subject get headlineSmall => has((t) => t.headlineSmall, 'headlineSmall'); + Subject get titleLarge => has((t) => t.titleLarge, 'titleLarge'); + Subject get titleMedium => has((t) => t.titleMedium, 'titleMedium'); + Subject get titleSmall => has((t) => t.titleSmall, 'titleSmall'); + Subject get bodyLarge => has((t) => t.bodyLarge, 'bodyLarge'); + Subject get bodyMedium => has((t) => t.bodyMedium, 'bodyMedium'); + Subject get bodySmall => has((t) => t.bodySmall, 'bodySmall'); + Subject get labelLarge => has((t) => t.labelLarge, 'labelLarge'); + Subject get labelMedium => has((t) => t.labelMedium, 'labelMedium'); + Subject get labelSmall => has((t) => t.labelSmall, 'labelSmall'); +} + +extension TypographyChecks on Subject { + Subject get black => has((t) => t.black, 'black'); + Subject get white => has((t) => t.white, 'white'); + Subject get englishLike => has((t) => t.englishLike, 'englishLike'); + Subject get dense => has((t) => t.dense, 'dense'); + Subject get tall => has((t) => t.tall, 'tall'); +} diff --git a/test/widgets/text_test.dart b/test/widgets/text_test.dart index 584950c736..e8c98481ed 100644 --- a/test/widgets/text_test.dart +++ b/test/widgets/text_test.dart @@ -7,6 +7,63 @@ import 'package:zulip/widgets/text.dart'; import '../flutter_checks.dart'; void main() { + group('zulipTypography', () { + Future getZulipTypography(WidgetTester tester, { + required bool platformRequestsBold, + }) async { + late final Typography result; + await tester.pumpWidget( + MediaQuery(data: MediaQueryData(boldText: platformRequestsBold), + child: Builder(builder: (context) { + result = zulipTypography(context); + return const SizedBox.shrink(); + }))); + return result; + } + + matchesFontFamilies(Subject it) => it + ..fontFamily.equals(kDefaultFontFamily) + ..fontFamilyFallback.isNotNull().deepEquals(defaultFontFamilyFallback); + + matchesWeight(FontWeight weight) => (Subject it) => it + ..fontWeight.equals(weight) + ..fontVariations.isNotNull().contains( + FontVariation('wght', wghtFromFontWeight(weight))); + + for (final platformRequestsBold in [false, true]) { + final description = platformRequestsBold + ? 'platform requests bold' + : 'platform does not request bold'; + testWidgets(description, (tester) async { + check(await getZulipTypography(tester, platformRequestsBold: platformRequestsBold)) + ..black.bodyMedium.isNotNull().which(matchesFontFamilies) + ..white.bodyMedium.isNotNull().which(matchesFontFamilies) + ..englishLike.bodyMedium.isNotNull().which( + matchesWeight(platformRequestsBold ? FontWeight.w700 : FontWeight.w400)) + ..dense.bodyMedium.isNotNull().which( + matchesWeight(platformRequestsBold ? FontWeight.w700 : FontWeight.w400)) + ..tall.bodyMedium.isNotNull().which( + matchesWeight(platformRequestsBold ? FontWeight.w700 : FontWeight.w400)); + }); + } + + test('Typography has the assumed fields', () { + check(Typography().toDiagnosticsNode().getProperties().map((n) => n.name).toList()) + .unorderedEquals(['black', 'white', 'englishLike', 'dense', 'tall']); + }); + }); + + test('_convertTextTheme: TextTheme has the assumed fields', () { + check(const TextTheme().toDiagnosticsNode().getProperties().map((n) => n.name).toList()) + .unorderedEquals([ + 'displayLarge', 'displayMedium', 'displaySmall', + 'headlineLarge', 'headlineMedium', 'headlineSmall', + 'titleLarge', 'titleMedium', 'titleSmall', + 'bodyLarge', 'bodyMedium', 'bodySmall', + 'labelLarge', 'labelMedium', 'labelSmall', + ]); + }); + group('weightVariableTextStyle', () { Future testWeights( String description, { @@ -50,16 +107,46 @@ void main() { platformRequestsBold: true, expectedFontVariations: const [FontVariation('wght', 700)], expectedFontWeight: FontWeight.bold); + testWeights('specific values; platform does not request bold', styleBuilder: (context) => weightVariableTextStyle(context, wght: 475, wghtIfPlatformRequestsBold: 675), platformRequestsBold: false, expectedFontVariations: const [FontVariation('wght', 475)], expectedFontWeight: FontWeight.w500); testWeights('specific values; platform requests bold', - platformRequestsBold: true, styleBuilder: (context) => weightVariableTextStyle(context, wght: 475, wghtIfPlatformRequestsBold: 675), + platformRequestsBold: true, expectedFontVariations: const [FontVariation('wght', 675)], expectedFontWeight: FontWeight.w700); + + testWeights('specific `wght`, default `wghtIfPlatformRequestsBold`; platform does not request bold', + styleBuilder: (context) => weightVariableTextStyle(context, wght: 475), + platformRequestsBold: false, + expectedFontVariations: const [FontVariation('wght', 475)], + expectedFontWeight: FontWeight.w500); + testWeights('specific `wght`, default `wghtIfPlatformRequestsBold`; platform requests bold', + styleBuilder: (context) => weightVariableTextStyle(context, wght: 475), + platformRequestsBold: true, + expectedFontVariations: const [FontVariation('wght', 775)], + expectedFontWeight: FontWeight.w800); + + testWeights('default `wght`, specific `wghtIfPlatformRequestsBold`; platform does not request bold', + styleBuilder: (context) => weightVariableTextStyle(context, wghtIfPlatformRequestsBold: 775), + platformRequestsBold: false, + expectedFontVariations: const [FontVariation('wght', 400)], + expectedFontWeight: FontWeight.normal); + testWeights('default `wght`, specific `wghtIfPlatformRequestsBold`; platform requests bold', + styleBuilder: (context) => weightVariableTextStyle(context, wghtIfPlatformRequestsBold: 775), + platformRequestsBold: true, + expectedFontVariations: const [FontVariation('wght', 775)], + expectedFontWeight: FontWeight.w800); + }); + + test('bolderWght', () { + check(bolderWght(1)).equals(301); + check(bolderWght(400)).equals(700); + check(bolderWght(600)).equals(900); + check(bolderWght(900)).equals(1000); }); test('clampVariableFontWeight: FontWeight has the assumed list of values', () {