Skip to content

Commit 38974d9

Browse files
chrisbobbegnprice
authored andcommitted
text: Set up to use variable fonts with a "wght" axis
For background on our interest in variable fonts with a "wght" axis, see zulip#65. One thing I noticed when adding such a font ('Source Code Pro', in a commit coming soon) is that by default, the text was drawn really lightly; in fact, Flutter seemed to be using the font's lightest weight by default. I guess I'd assumed Flutter would pick a "normal" weight by default, as it does for non-variable fonts. Possibly that's just because Flutter hasn't fully caught up with this recent development in fonts. But also, I'm not sure if these new fonts all *have* mappings from "normal", "bold", etc., to values on their "wght" axes...whether such mappings are standard/predictable, or at least declared by the font in structured metadata. Anyway, in my design here, it means that if you want to use one of these fonts, you still need an incantation (weightVariableTextStyle) to render text where a normal weight makes sense. Hopefully that's not too burdensome, but it comes with a benefit: as long as that's the norm, I think we're unlikely to misuse `fontWeight` to control glyphs that want to be controlled by "wght". Much of the complexity in this commit comes from handling glyphs that need to be rendered in a fallback font that doesn't have a "wght" axis; see clampVariableFontWeight and how we use that. It means those glyphs will have approximately the weight of the other glyphs...not essential, but nice to have. Some complexity comes from extending Flutter's support for the "bold-text" accessibility setting, so that it applies to these new fonts that use "wght". Ideally we wouldn't regress on our support for that platform setting, but in this design, it means callers have to have a BuildContext on hand; hmm.
1 parent 010c31a commit 38974d9

File tree

3 files changed

+208
-0
lines changed

3 files changed

+208
-0
lines changed

lib/widgets/text.dart

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:io';
2+
import 'dart:ui';
23
import 'package:flutter/widgets.dart';
34

45
/// A mergeable [TextStyle] with 'Source Code Pro' and platform-aware fallbacks.
@@ -17,3 +18,80 @@ final TextStyle kMonospaceTextStyle = TextStyle(
1718

1819
inherit: true,
1920
);
21+
22+
/// A mergeable [TextStyle] to use when the preferred font has a "wght" axis.
23+
///
24+
/// Some variable fonts can be controlled on a "wght" axis.
25+
/// Use this to set a value on that axis. It uses [TextStyle.fontVariations],
26+
/// along with a [TextStyle.fontWeight] that approximates the given "wght"
27+
/// for the sake of glyphs that need to be rendered by a fallback font
28+
/// (which might not offer a "wght" axis).
29+
///
30+
/// Use this even to specify normal-weight text, by omitting `wght`; then,
31+
/// [FontWeight.normal.value] will be used. No other layer applies a default,
32+
/// so if you don't use this, you may e.g. get the font's lightest weight.
33+
///
34+
/// Pass [context] to respect a platform request to draw bold text for
35+
/// accessibility (see [MediaQueryData.boldText]). This handles that request by
36+
/// using [wghtIfPlatformRequestsBold] or if that's null, [FontWeight.bold.value].
37+
///
38+
/// Example:
39+
///
40+
/// ```dart
41+
/// someTextStyle.merge(weightVariableTextStyle(context, wght: 250)
42+
/// ```
43+
///
44+
/// See also [FontVariation] for more background on variable fonts.
45+
// TODO(a11y) make `context` required when callers can adapt?
46+
TextStyle weightVariableTextStyle(BuildContext? context, {
47+
double? wght,
48+
double? wghtIfPlatformRequestsBold,
49+
}) {
50+
assert((wght != null) == (wghtIfPlatformRequestsBold != null));
51+
double value = wght ?? FontWeight.normal.value.toDouble();
52+
if (context != null && MediaQuery.of(context).boldText) {
53+
// The framework has a condition on [MediaQueryData.boldText]
54+
// in the [Text] widget, but that only affects `fontWeight`.
55+
// [Text] doesn't know where to land on the chosen font's "wght" axis if any,
56+
// and indeed it doesn't seem updated to be aware of variable fonts at all.
57+
value = wghtIfPlatformRequestsBold ?? FontWeight.bold.value.toDouble();
58+
}
59+
assert(value >= 1 && value <= 1000); // https://fonts.google.com/variablefonts#axis-definitions
60+
61+
return TextStyle(
62+
fontVariations: [FontVariation('wght', value)],
63+
64+
// This use of `fontWeight` shouldn't affect glyphs in the preferred,
65+
// "wght"-axis font. If it does, see for debugging:
66+
// https://github.com/zulip/zulip-flutter/issues/65#issuecomment-1550666764
67+
fontWeight: clampVariableFontWeight(value),
68+
69+
inherit: true);
70+
}
71+
72+
/// Find the nearest [FontWeight] constant for a variable-font "wght"-axis value.
73+
///
74+
/// Use this for a reasonable [TextStyle.fontWeight] for glyphs that need to be
75+
/// rendered by a fallback font that doesn't have a "wght" axis.
76+
///
77+
/// See also [FontVariation] for background on variable fonts.
78+
FontWeight clampVariableFontWeight(double wght) {
79+
if (wght < 450) {
80+
if (wght < 250) {
81+
if (wght < 150) return FontWeight.w100; // ignore_for_file: curly_braces_in_flow_control_structures
82+
else return FontWeight.w200;
83+
} else {
84+
if (wght < 350) return FontWeight.w300;
85+
else return FontWeight.w400;
86+
}
87+
} else {
88+
if (wght < 650) {
89+
if (wght < 550) return FontWeight.w500;
90+
else return FontWeight.w600;
91+
} else {
92+
if (wght < 750) return FontWeight.w700;
93+
else if (wght < 850) return FontWeight.w800;
94+
else return FontWeight.w900;
95+
}
96+
}
97+
}

test/flutter_checks.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// `package:checks`-related extensions for the Flutter framework.
2+
import 'dart:ui';
3+
4+
import 'package:checks/checks.dart';
5+
import 'package:flutter/painting.dart';
6+
7+
extension TextStyleChecks on Subject<TextStyle> {
8+
Subject<bool> get inherit => has((t) => t.inherit, 'inherit');
9+
Subject<List<FontVariation>?> get fontVariations => has((t) => t.fontVariations, 'fontVariations');
10+
Subject<FontWeight?> get fontWeight => has((t) => t.fontWeight, 'fontWeight');
11+
12+
// TODO others
13+
}

test/widgets/text_test.dart

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import 'dart:ui';
2+
3+
import 'package:checks/checks.dart';
4+
import 'package:flutter/material.dart';
5+
import 'package:flutter_test/flutter_test.dart';
6+
import 'package:zulip/widgets/text.dart';
7+
8+
import '../flutter_checks.dart';
9+
10+
void main() {
11+
group('weightVariableTextStyle', () {
12+
Future<void> testWeights(
13+
String description, {
14+
required TextStyle Function(BuildContext context) styleBuilder,
15+
bool platformRequestsBold = false,
16+
required List<FontVariation> expectedFontVariations,
17+
required FontWeight expectedFontWeight,
18+
}) async {
19+
testWidgets(description, (WidgetTester tester) async {
20+
await tester.pumpWidget(
21+
MaterialApp(
22+
home: MediaQuery(
23+
data: MediaQueryData(boldText: platformRequestsBold),
24+
child: Builder(builder: (context) => Text('', style: styleBuilder(context))))));
25+
26+
final TextStyle? style = tester.widget<Text>(find.byType(Text)).style;
27+
check(style)
28+
.isNotNull()
29+
..inherit.isTrue()
30+
..fontVariations.isNotNull().deepEquals(expectedFontVariations)
31+
..fontWeight.isNotNull().equals(expectedFontWeight);
32+
});
33+
}
34+
35+
testWeights('no context passed; default wght values',
36+
styleBuilder: (context) => weightVariableTextStyle(null),
37+
expectedFontVariations: const [FontVariation('wght', 400)],
38+
expectedFontWeight: FontWeight.normal);
39+
testWeights('no context passed; specific wght',
40+
styleBuilder: (context) => weightVariableTextStyle(null, wght: 225, wghtIfPlatformRequestsBold: 425),
41+
expectedFontVariations: const [FontVariation('wght', 225)],
42+
expectedFontWeight: FontWeight.w200);
43+
44+
testWeights('default values; platform does not request bold',
45+
styleBuilder: (context) => weightVariableTextStyle(context),
46+
platformRequestsBold: false,
47+
expectedFontVariations: const [FontVariation('wght', 400)],
48+
expectedFontWeight: FontWeight.normal);
49+
testWeights('default values; platform requests bold',
50+
styleBuilder: (context) => weightVariableTextStyle(context),
51+
platformRequestsBold: true,
52+
expectedFontVariations: const [FontVariation('wght', 700)],
53+
expectedFontWeight: FontWeight.bold);
54+
testWeights('specific values; platform does not request bold',
55+
styleBuilder: (context) => weightVariableTextStyle(context, wght: 475, wghtIfPlatformRequestsBold: 675),
56+
platformRequestsBold: false,
57+
expectedFontVariations: const [FontVariation('wght', 475)],
58+
expectedFontWeight: FontWeight.w500);
59+
testWeights('specific values; platform requests bold',
60+
platformRequestsBold: true,
61+
styleBuilder: (context) => weightVariableTextStyle(context, wght: 475, wghtIfPlatformRequestsBold: 675),
62+
expectedFontVariations: const [FontVariation('wght', 675)],
63+
expectedFontWeight: FontWeight.w700);
64+
});
65+
66+
test('clampVariableFontWeight: FontWeight has the assumed list of values', () {
67+
// Implementation assumes specific FontWeight values; we should
68+
// adapt if these change in a new Flutter version.
69+
check(FontWeight.values).deepEquals([
70+
FontWeight.w100, FontWeight.w200, FontWeight.w300,
71+
FontWeight.w400, FontWeight.w500, FontWeight.w600,
72+
FontWeight.w700, FontWeight.w800, FontWeight.w900,
73+
]);
74+
});
75+
76+
test('clampVariableFontWeight', () {
77+
check(clampVariableFontWeight(1)) .equals(FontWeight.w100);
78+
check(clampVariableFontWeight(99)) .equals(FontWeight.w100);
79+
check(clampVariableFontWeight(100)) .equals(FontWeight.w100);
80+
check(clampVariableFontWeight(101)) .equals(FontWeight.w100);
81+
82+
check(clampVariableFontWeight(199)) .equals(FontWeight.w200);
83+
check(clampVariableFontWeight(200)) .equals(FontWeight.w200);
84+
check(clampVariableFontWeight(201)) .equals(FontWeight.w200);
85+
86+
check(clampVariableFontWeight(250)) .equals(FontWeight.w300);
87+
check(clampVariableFontWeight(299)) .equals(FontWeight.w300);
88+
check(clampVariableFontWeight(300)) .equals(FontWeight.w300);
89+
check(clampVariableFontWeight(301)) .equals(FontWeight.w300);
90+
91+
check(clampVariableFontWeight(399)) .equals(FontWeight.w400);
92+
check(clampVariableFontWeight(400)) .equals(FontWeight.w400);
93+
check(clampVariableFontWeight(401)) .equals(FontWeight.w400);
94+
95+
check(clampVariableFontWeight(499)) .equals(FontWeight.w500);
96+
check(clampVariableFontWeight(500)) .equals(FontWeight.w500);
97+
check(clampVariableFontWeight(501)) .equals(FontWeight.w500);
98+
99+
check(clampVariableFontWeight(599)) .equals(FontWeight.w600);
100+
check(clampVariableFontWeight(600)) .equals(FontWeight.w600);
101+
check(clampVariableFontWeight(601)) .equals(FontWeight.w600);
102+
103+
check(clampVariableFontWeight(699)) .equals(FontWeight.w700);
104+
check(clampVariableFontWeight(700)) .equals(FontWeight.w700);
105+
check(clampVariableFontWeight(701)) .equals(FontWeight.w700);
106+
107+
check(clampVariableFontWeight(799)) .equals(FontWeight.w800);
108+
check(clampVariableFontWeight(800)) .equals(FontWeight.w800);
109+
check(clampVariableFontWeight(801)) .equals(FontWeight.w800);
110+
111+
check(clampVariableFontWeight(899)) .equals(FontWeight.w900);
112+
check(clampVariableFontWeight(900)) .equals(FontWeight.w900);
113+
check(clampVariableFontWeight(901)) .equals(FontWeight.w900);
114+
check(clampVariableFontWeight(999)) .equals(FontWeight.w900);
115+
check(clampVariableFontWeight(1000)) .equals(FontWeight.w900);
116+
});
117+
}

0 commit comments

Comments
 (0)