diff --git a/lib/widgets/emoji_reaction.dart b/lib/widgets/emoji_reaction.dart index ba7fc7a4a4e..2c63fef9c4b 100644 --- a/lib/widgets/emoji_reaction.dart +++ b/lib/widgets/emoji_reaction.dart @@ -153,6 +153,7 @@ class ReactionChip extends StatelessWidget { // which we learn about especially late). final maxLabelWidth = (maxRowWidth - 6) * 0.75; // 6 is padding + final labelScaler = _labelTextScalerClamped(context); return Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, @@ -168,9 +169,13 @@ class ReactionChip extends StatelessWidget { constraints: BoxConstraints(maxWidth: maxLabelWidth), child: Text( textWidthBasis: TextWidthBasis.longestLine, - textScaler: _labelTextScalerClamped(context), + textScaler: labelScaler, style: TextStyle( fontSize: (14 * 0.90), + letterSpacing: proportionalLetterSpacing(context, + kButtonTextLetterSpacingProportion, + baseFontSize: (14 * 0.90), + textScaler: labelScaler), height: 13 / (14 * 0.90), color: labelColor, ).merge(weightVariableTextStyle(context, diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index a102fb13e52..f3c8b63ba0c 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -484,8 +484,10 @@ class MarkAsReadWidget extends StatelessWidget { // [zulipTypography]… Theme.of(context).textTheme.labelLarge! // …then clobber some attributes to follow Figma: - .merge(const TextStyle( + .merge(TextStyle( fontSize: 18, + letterSpacing: proportionalLetterSpacing(context, + kButtonTextLetterSpacingProportion, baseFontSize: 18), height: (23 / 18)) .merge(weightVariableTextStyle(context, wght: 400))), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(7)), diff --git a/lib/widgets/text.dart b/lib/widgets/text.dart index a3c82aceb97..d0fbea35461 100644 --- a/lib/widgets/text.dart +++ b/lib/widgets/text.dart @@ -40,6 +40,14 @@ Typography zulipTypography(BuildContext context) { result = _convertTextTheme(result, (maybeInputStyle, _) => maybeInputStyle?.merge(const TextStyle(letterSpacing: 0))); + result = result.copyWith( + labelLarge: result.labelLarge!.copyWith( + fontSize: 14.0, // (should be unchanged; restated here for explicitness) + letterSpacing: proportionalLetterSpacing(context, + kButtonTextLetterSpacingProportion, baseFontSize: 14.0), + ), + ); + return result; } @@ -167,6 +175,8 @@ final TextStyle kMonospaceTextStyle = TextStyle( inherit: true, ); +const kButtonTextLetterSpacingProportion = 0.01; + /// A mergeable [TextStyle] to use when the preferred font has a "wght" axis. /// /// Some variable fonts can be controlled on a "wght" axis. diff --git a/test/flutter_checks.dart b/test/flutter_checks.dart index bc7e9d83a70..20a38225dc5 100644 --- a/test/flutter_checks.dart +++ b/test/flutter_checks.dart @@ -65,6 +65,7 @@ extension TextFieldChecks on Subject { extension TextStyleChecks on Subject { Subject get inherit => has((t) => t.inherit, 'inherit'); + Subject get fontSize => has((t) => t.fontSize, 'fontSize'); Subject get fontWeight => has((t) => t.fontWeight, 'fontWeight'); Subject get letterSpacing => has((t) => t.letterSpacing, 'letterSpacing'); Subject?> get fontVariations => has((t) => t.fontVariations, 'fontVariations'); diff --git a/test/widgets/text_test.dart b/test/widgets/text_test.dart index 9fb03b4bf9b..e4e683520f8 100644 --- a/test/widgets/text_test.dart +++ b/test/widgets/text_test.dart @@ -46,11 +46,14 @@ void main() { }); } - testWidgets('zero letter spacing', (tester) async { + testWidgets('letter spacing', (tester) async { check(await getZulipTypography(tester, platformRequestsBold: false)) ..englishLike.bodyMedium.isNotNull().letterSpacing.equals(0) + ..englishLike.labelLarge.isNotNull().letterSpacing.equals(0.14) ..dense.bodyMedium.isNotNull().letterSpacing.equals(0) - ..tall.bodyMedium.isNotNull().letterSpacing.equals(0); + ..dense.labelLarge.isNotNull().letterSpacing.equals(0.14) + ..tall.bodyMedium.isNotNull().letterSpacing.equals(0) + ..tall.labelLarge.isNotNull().letterSpacing.equals(0.14); }); test('Typography has the assumed fields', () { diff --git a/test/widgets/theme_test.dart b/test/widgets/theme_test.dart new file mode 100644 index 00000000000..8ba10c45282 --- /dev/null +++ b/test/widgets/theme_test.dart @@ -0,0 +1,78 @@ +import 'package:checks/checks.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zulip/widgets/text.dart'; +import 'package:zulip/widgets/theme.dart'; + +import '../flutter_checks.dart'; + +void main() { + group('button text size and letter spacing', () { + Future doCheck( + String description, { + required Widget Function(BuildContext context, String text) buttonBuilder, + double? ambientTextScaleFactor, + }) async { + testWidgets(description, (WidgetTester tester) async { + if (ambientTextScaleFactor != null) { + tester.platformDispatcher.textScaleFactorTestValue = ambientTextScaleFactor; + addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue); + } + const buttonText = 'Zulip'; + double? expectedFontSize; + double? expectedLetterSpacing; + await tester.pumpWidget( + Builder(builder: (context) => MaterialApp( + theme: zulipThemeData(context), + home: Builder(builder: (context) { + expectedFontSize = Theme.of(context).textTheme.labelLarge!.fontSize!; + expectedLetterSpacing = proportionalLetterSpacing(context, + 0.01, baseFontSize: expectedFontSize!); + return Builder(builder: (context) => + buttonBuilder(context, buttonText)); + })))); + + final text = tester.renderObject(find.text(buttonText)).text; + check(text.style!) + ..fontSize.equals(expectedFontSize) + ..letterSpacing.equals(expectedLetterSpacing); + }); + } + + doCheck('with device text size adjusted', + ambientTextScaleFactor: 2.0, + buttonBuilder: (context, text) => ElevatedButton(onPressed: () {}, + child: Text(text))); + + doCheck('ElevatedButton', + buttonBuilder: (context, text) => ElevatedButton(onPressed: () {}, + child: Text(text))); + + doCheck('FilledButton', + buttonBuilder: (context, text) => FilledButton(onPressed: () {}, + child: Text(text))); + + // IconButton can't have text; skip + + doCheck('MenuItemButton', + buttonBuilder: (context, text) => MenuItemButton(onPressed: () {}, + child: Text(text))); + + doCheck('SubmenuButton', + buttonBuilder: (context, text) => SubmenuButton(menuChildren: const [], + child: Text(text))); + + doCheck('OutlinedButton', + buttonBuilder: (context, text) => OutlinedButton(onPressed: () {}, + child: Text(text))); + + doCheck('SegmentedButton', + buttonBuilder: (context, text) => SegmentedButton(selected: const {1}, + segments: [ButtonSegment(value: 1, label: Text(text))])); + + doCheck('TextButton', + buttonBuilder: (context, text) => TextButton(onPressed: () {}, + child: Text(text))); + }); +}