Skip to content

Commit 0c9986a

Browse files
committed
text: Set up to use variable fonts with a "wght" axis
For background on our interest in variable fonts, see zulip#65.
1 parent 79aa536 commit 0c9986a

File tree

2 files changed

+207
-0
lines changed

2 files changed

+207
-0
lines changed

lib/widgets/text.dart

Lines changed: 96 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,98 @@ 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 [wghtPlatformRequestsBold] 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+
int? wght,
48+
int? wghtPlatformRequestsBold,
49+
}) {
50+
int value = wght ?? FontWeight.normal.value;
51+
if (context != null && MediaQuery.of(context).boldText) {
52+
// The framework has a condition on [MediaQueryData.boldText]
53+
// in the [Text] widget, but that only affects `fontWeight`.
54+
// [Text] doesn't know where to land on the chosen font's "wght" axis if any,
55+
// and indeed it doesn't seem updated to be aware of variable fonts at all.
56+
value = wghtPlatformRequestsBold ?? FontWeight.bold.value;
57+
}
58+
assert(value >= 1 && value <= 1000); // https://fonts.google.com/variablefonts#axis-definitions
59+
60+
return TextStyle(
61+
fontVariations: [FontVariation('wght', value.toDouble())],
62+
63+
// This use of `fontWeight` shouldn't affect glyphs in the preferred,
64+
// "wght"-axis font. If it does, see for debugging:
65+
// https://github.com/zulip/zulip-flutter/issues/65#issuecomment-1550666764
66+
fontWeight: clampVariableFontWeight(value),
67+
68+
inherit: true);
69+
}
70+
71+
/// Find the nearest [FontWeight] constant for a variable-font "wght"-axis value.
72+
///
73+
/// Use this for a reasonable [TextStyle.fontWeight] for glyphs that need to be
74+
/// rendered by a fallback font that doesn't have a "wght" axis.
75+
///
76+
/// See also [FontVariation] for background on variable fonts.
77+
FontWeight clampVariableFontWeight(int wght) {
78+
const values = FontWeight.values;
79+
assert((() { // `values` is non-empty and sorted
80+
FontWeight? last;
81+
return values.isNotEmpty && values.every((fontWeight) {
82+
final result = (last == null ||
83+
(fontWeight.index > last!.index && fontWeight.value > last!.value));
84+
last = fontWeight;
85+
return result;
86+
});
87+
})());
88+
89+
FontWeight? lower;
90+
FontWeight? upper;
91+
for (final fontWeight in values) {
92+
if (fontWeight.value > wght) {
93+
upper = fontWeight;
94+
break;
95+
}
96+
lower = fontWeight;
97+
}
98+
99+
if (upper == null) {
100+
return lower!;
101+
}
102+
if (lower == null) {
103+
return upper;
104+
}
105+
106+
final diffLower = wght - lower.value;
107+
final diffUpper = upper.value - wght;
108+
if (diffLower < diffUpper) {
109+
return lower;
110+
} else if (diffLower > diffUpper) {
111+
return upper;
112+
} else {
113+
return lower;
114+
}
115+
}

test/widgets/text_test.dart

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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+
void main() {
9+
group('weightVariableTextStyle', () {
10+
Future<void> runCheck(
11+
String description,
12+
{
13+
required TextStyle Function(BuildContext context) styleBuilder,
14+
bool platformRequestsBold = false,
15+
required List<FontVariation> expectedFontVariations,
16+
required FontWeight expectedFontWeight,
17+
}
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+
..has((style) => style.inherit, 'inherit').isTrue()
30+
..has((style) => style.fontVariations, 'fontVariations')
31+
.isNotNull()
32+
.deepEquals(expectedFontVariations)
33+
..has((style) => style.fontWeight, 'fontWeight')
34+
.isNotNull()
35+
.equals(expectedFontWeight);
36+
});
37+
}
38+
39+
runCheck('no context passed; default wght values',
40+
styleBuilder: (context) => weightVariableTextStyle(null),
41+
expectedFontVariations: const [FontVariation('wght', 400)],
42+
expectedFontWeight: FontWeight.normal);
43+
runCheck('no context passed; specific wght',
44+
styleBuilder: (context) => weightVariableTextStyle(null, wght: 225),
45+
expectedFontVariations: const [FontVariation('wght', 225)],
46+
expectedFontWeight: FontWeight.w200);
47+
48+
runCheck('default values; platform does not request bold',
49+
styleBuilder: (context) => weightVariableTextStyle(context),
50+
platformRequestsBold: false,
51+
expectedFontVariations: const [FontVariation('wght', 400)],
52+
expectedFontWeight: FontWeight.normal);
53+
runCheck('default values; platform requests bold',
54+
styleBuilder: (context) => weightVariableTextStyle(context),
55+
platformRequestsBold: true,
56+
expectedFontVariations: const [FontVariation('wght', 700)],
57+
expectedFontWeight: FontWeight.bold);
58+
runCheck('specific values; platform does not request bold',
59+
styleBuilder: (context) => weightVariableTextStyle(context, wght: 475, wghtPlatformRequestsBold: 675),
60+
platformRequestsBold: false,
61+
expectedFontVariations: const [FontVariation('wght', 475)],
62+
expectedFontWeight: FontWeight.w500);
63+
runCheck('specific values; platform requests bold',
64+
platformRequestsBold: true,
65+
styleBuilder: (context) => weightVariableTextStyle(context, wght: 475, wghtPlatformRequestsBold: 675),
66+
expectedFontVariations: const [FontVariation('wght', 675)],
67+
expectedFontWeight: FontWeight.w700);
68+
});
69+
70+
test('clampVariableFontWeight', () {
71+
check(clampVariableFontWeight(1)) .equals(FontWeight.w100);
72+
check(clampVariableFontWeight(99)) .equals(FontWeight.w100);
73+
check(clampVariableFontWeight(100)) .equals(FontWeight.w100);
74+
check(clampVariableFontWeight(101)) .equals(FontWeight.w100);
75+
76+
check(clampVariableFontWeight(199)) .equals(FontWeight.w200);
77+
check(clampVariableFontWeight(200)) .equals(FontWeight.w200);
78+
check(clampVariableFontWeight(201)) .equals(FontWeight.w200);
79+
check(clampVariableFontWeight(250)) .equals(FontWeight.w200);
80+
81+
check(clampVariableFontWeight(299)) .equals(FontWeight.w300);
82+
check(clampVariableFontWeight(300)) .equals(FontWeight.w300);
83+
check(clampVariableFontWeight(301)) .equals(FontWeight.w300);
84+
85+
check(clampVariableFontWeight(399)) .equals(FontWeight.w400);
86+
check(clampVariableFontWeight(400)) .equals(FontWeight.w400);
87+
check(clampVariableFontWeight(401)) .equals(FontWeight.w400);
88+
89+
check(clampVariableFontWeight(499)) .equals(FontWeight.w500);
90+
check(clampVariableFontWeight(500)) .equals(FontWeight.w500);
91+
check(clampVariableFontWeight(501)) .equals(FontWeight.w500);
92+
93+
check(clampVariableFontWeight(599)) .equals(FontWeight.w600);
94+
check(clampVariableFontWeight(600)) .equals(FontWeight.w600);
95+
check(clampVariableFontWeight(601)) .equals(FontWeight.w600);
96+
97+
check(clampVariableFontWeight(699)) .equals(FontWeight.w700);
98+
check(clampVariableFontWeight(700)) .equals(FontWeight.w700);
99+
check(clampVariableFontWeight(701)) .equals(FontWeight.w700);
100+
101+
check(clampVariableFontWeight(799)) .equals(FontWeight.w800);
102+
check(clampVariableFontWeight(800)) .equals(FontWeight.w800);
103+
check(clampVariableFontWeight(801)) .equals(FontWeight.w800);
104+
105+
check(clampVariableFontWeight(899)) .equals(FontWeight.w900);
106+
check(clampVariableFontWeight(900)) .equals(FontWeight.w900);
107+
check(clampVariableFontWeight(901)) .equals(FontWeight.w900);
108+
check(clampVariableFontWeight(999)) .equals(FontWeight.w900);
109+
check(clampVariableFontWeight(1000)) .equals(FontWeight.w900);
110+
});
111+
}

0 commit comments

Comments
 (0)