Skip to content

Commit d882f50

Browse files
committed
ui: Set letter spacing to 1% in buttons
TextTheme.labelLarge is the default label style in all the various Material buttons; see: [ElevatedButton.defaultStyleOf], [FilledButton.defaultStyleOf], [MenuItemButton.defaultStyleOf], [MenuItemButton.defaultStyleOf], [OutlinedButton.defaultStyleOf], `defaults` in [SegmentedButton.build], [TextButton.defaultStyleOf] . Since our buttons are all Material buttons, changing this will effectively "set letter spacing to 1% in buttons", which is #548. (...Actually, I guess there's one kind of button that's not a Material button: reaction chips in the message list. So, adjust those separately, also in this commit.) Greg mentioned, when suggesting this approach, > In principle that has a slightly broader effect than specified in > #548, in that it affects any other Material widgets that use the > `labelLarge` text style. But fundamentally those are widgets that > the Material designers intended to have a similar text style to > that of buttons (even though they're not called "buttons"), so > it's likely that that'll be the right answer anyway. > If we end up with such a widget where Vlad wants a different > spacing, we can address that then… and I think it's fairly likely > that in that case he'd want something that wasn't the Material > widget in any event. Which makes sense and works for me. Fixes: #548
1 parent af20651 commit d882f50

File tree

6 files changed

+96
-4
lines changed

6 files changed

+96
-4
lines changed

lib/widgets/emoji_reaction.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ class ReactionChip extends StatelessWidget {
153153
// which we learn about especially late).
154154
final maxLabelWidth = (maxRowWidth - 6) * 0.75; // 6 is padding
155155

156+
final labelScaler = _labelTextScalerClamped(context);
156157
return Row(
157158
mainAxisSize: MainAxisSize.min,
158159
crossAxisAlignment: CrossAxisAlignment.center,
@@ -168,9 +169,13 @@ class ReactionChip extends StatelessWidget {
168169
constraints: BoxConstraints(maxWidth: maxLabelWidth),
169170
child: Text(
170171
textWidthBasis: TextWidthBasis.longestLine,
171-
textScaler: _labelTextScalerClamped(context),
172+
textScaler: labelScaler,
172173
style: TextStyle(
173174
fontSize: (14 * 0.90),
175+
letterSpacing: proportionalLetterSpacing(context,
176+
kButtonTextLetterSpacingProportion,
177+
baseFontSize: (14 * 0.90),
178+
textScaler: labelScaler),
174179
height: 13 / (14 * 0.90),
175180
color: labelColor,
176181
).merge(weightVariableTextStyle(context,

lib/widgets/message_list.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,8 +484,10 @@ class MarkAsReadWidget extends StatelessWidget {
484484
// [zulipTypography]…
485485
Theme.of(context).textTheme.labelLarge!
486486
// …then clobber some attributes to follow Figma:
487-
.merge(const TextStyle(
487+
.merge(TextStyle(
488488
fontSize: 18,
489+
letterSpacing: proportionalLetterSpacing(context,
490+
kButtonTextLetterSpacingProportion, baseFontSize: 18),
489491
height: (23 / 18))
490492
.merge(weightVariableTextStyle(context, wght: 400))),
491493
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(7)),

lib/widgets/text.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ Typography zulipTypography(BuildContext context) {
4040
result = _convertTextTheme(result, (maybeInputStyle, _) =>
4141
maybeInputStyle?.merge(const TextStyle(letterSpacing: 0)));
4242

43+
result = result.copyWith(
44+
labelLarge: result.labelLarge!.copyWith(
45+
fontSize: 14.0, // (should be unchanged; restated here for explicitness)
46+
letterSpacing: proportionalLetterSpacing(context,
47+
kButtonTextLetterSpacingProportion, baseFontSize: 14.0),
48+
),
49+
);
50+
4351
return result;
4452
}
4553

@@ -167,6 +175,8 @@ final TextStyle kMonospaceTextStyle = TextStyle(
167175
inherit: true,
168176
);
169177

178+
const kButtonTextLetterSpacingProportion = 0.01;
179+
170180
/// A mergeable [TextStyle] to use when the preferred font has a "wght" axis.
171181
///
172182
/// Some variable fonts can be controlled on a "wght" axis.

test/flutter_checks.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ extension TextFieldChecks on Subject<TextField> {
6565

6666
extension TextStyleChecks on Subject<TextStyle> {
6767
Subject<bool> get inherit => has((t) => t.inherit, 'inherit');
68+
Subject<double?> get fontSize => has((t) => t.fontSize, 'fontSize');
6869
Subject<FontWeight?> get fontWeight => has((t) => t.fontWeight, 'fontWeight');
6970
Subject<double?> get letterSpacing => has((t) => t.letterSpacing, 'letterSpacing');
7071
Subject<List<FontVariation>?> get fontVariations => has((t) => t.fontVariations, 'fontVariations');

test/widgets/text_test.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,14 @@ void main() {
5454
});
5555
}
5656

57-
testWidgets('zero letter spacing', (tester) async {
57+
testWidgets('letter spacing', (tester) async {
5858
check(await getZulipTypography(tester, platformRequestsBold: false))
5959
..englishLike.bodyMedium.isNotNull().letterSpacing.equals(0)
60+
..englishLike.labelLarge.isNotNull().letterSpacing.equals(0.14)
6061
..dense.bodyMedium.isNotNull().letterSpacing.equals(0)
61-
..tall.bodyMedium.isNotNull().letterSpacing.equals(0);
62+
..dense.labelLarge.isNotNull().letterSpacing.equals(0.14)
63+
..tall.bodyMedium.isNotNull().letterSpacing.equals(0)
64+
..tall.labelLarge.isNotNull().letterSpacing.equals(0.14);
6265
});
6366

6467
test('Typography has the assumed fields', () {

test/widgets/theme_test.dart

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import 'package:checks/checks.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter/rendering.dart';
4+
import 'package:flutter_test/flutter_test.dart';
5+
import 'package:zulip/widgets/text.dart';
6+
import 'package:zulip/widgets/theme.dart';
7+
8+
import '../flutter_checks.dart';
9+
10+
void main() {
11+
group('button text size and letter spacing', () {
12+
const buttonText = 'Zulip';
13+
14+
Future<void> doCheck(
15+
String description, {
16+
required Widget button,
17+
double? ambientTextScaleFactor,
18+
}) async {
19+
testWidgets(description, (WidgetTester tester) async {
20+
if (ambientTextScaleFactor != null) {
21+
tester.platformDispatcher.textScaleFactorTestValue = ambientTextScaleFactor;
22+
addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue);
23+
}
24+
late final double expectedFontSize;
25+
late final double expectedLetterSpacing;
26+
await tester.pumpWidget(
27+
Builder(builder: (context) => MaterialApp(
28+
theme: zulipThemeData(context),
29+
home: Builder(builder: (context) {
30+
expectedFontSize = Theme.of(context).textTheme.labelLarge!.fontSize!;
31+
expectedLetterSpacing = proportionalLetterSpacing(context,
32+
0.01, baseFontSize: expectedFontSize);
33+
return button;
34+
}))));
35+
36+
final text = tester.renderObject<RenderParagraph>(find.text(buttonText)).text;
37+
check(text.style!)
38+
..fontSize.equals(expectedFontSize)
39+
..letterSpacing.equals(expectedLetterSpacing);
40+
});
41+
}
42+
43+
doCheck('with device text size adjusted',
44+
ambientTextScaleFactor: 2.0,
45+
button: ElevatedButton(onPressed: () {}, child: const Text(buttonText)));
46+
47+
doCheck('ElevatedButton',
48+
button: ElevatedButton(onPressed: () {}, child: const Text(buttonText)));
49+
50+
doCheck('FilledButton',
51+
button: FilledButton(onPressed: () {}, child: const Text(buttonText)));
52+
53+
// IconButton can't have text; skip
54+
55+
doCheck('MenuItemButton',
56+
button: MenuItemButton(onPressed: () {}, child: const Text(buttonText)));
57+
58+
doCheck('SubmenuButton',
59+
button: const SubmenuButton(menuChildren: [], child: Text(buttonText)));
60+
61+
doCheck('OutlinedButton',
62+
button: OutlinedButton(onPressed: () {}, child: const Text(buttonText)));
63+
64+
doCheck('SegmentedButton',
65+
button: SegmentedButton(selected: const {1},
66+
segments: const [ButtonSegment(value: 1, label: Text(buttonText))]));
67+
68+
doCheck('TextButton',
69+
button: TextButton(onPressed: () {}, child: const Text(buttonText)));
70+
});
71+
}

0 commit comments

Comments
 (0)