-
Notifications
You must be signed in to change notification settings - Fork 306
text: Bundle 'Source Code Pro' font in the app; use it for code blocks/spans in messages #94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
text: Bundle 'Source Code Pro' font in the app; use it for code blocks/spans in messages #94
Conversation
Tested on iOS and Android, and the text appears in the new font and with an appropriate-looking weight. |
f6d744a
to
f62df19
Compare
Just updated with a newer version of the font. As @andersk points out:
I also updated this to add an entry for this font's license in |
f62df19
to
91cd5bd
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for taking care of this! And thanks @andersk for spotting those opportunities to use better versions of the fonts.
This approach looks good; specific comments below.
lib/widgets/text.dart
Outdated
for (final fontWeight in values) { | ||
if (fontWeight.value > wght) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's only 9 of these possible values, in a fixed list of them. So it'd be simpler to just write out some comparisons explicitly:
if (wght < 450) {
if (wght < 250) {
if (wght < 150) return FontWeight.w100;
else return FontWeight.w200;
} else {
// …
}
} else {
// …
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking about being agnostic to what the list was (except that it's non-empty and sorted), in case it changes in a new Flutter version. But I guess it's not super likely to change, and also we can have a test or an assert
to alert us if it changes.
lib/widgets/text.dart
Outdated
// TODO(a11y) make `context` required when callers can adapt? | ||
TextStyle weightVariableTextStyle(BuildContext? context, { | ||
int? wght, | ||
int? wghtPlatformRequestsBold, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the name of this parameter and the dartdoc above, I'm not sure what it means.
Ah, I see — it's the weight to use if the platform requests bold. Adding "if" to the name would help a lot, I think, and not really be materially longer than it is already.
lib/widgets/text.dart
Outdated
int? wght, | ||
int? wghtPlatformRequestsBold, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like it'd be a bug for a caller to specify one of these but not the other. Right?
In that case, would be good to specify that and have an assertion: assert((wght != null) == (wghtIfPlatformRequestsBold) != null));
.
lib/widgets/content.dart
Outdated
.merge(const TextStyle( | ||
backgroundColor: Color(0xffeeeeee), | ||
fontSize: 0.825 * kBaseFontSize)) | ||
.merge(weightVariableTextStyle(null)), // TODO(a11y) pass a BuildContext |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, the TextStyle for this span had been a const
, but now there's a couple of merge
calls going on, plus everything that's in weightVariableTextStyle
. That feels like more computation than I want to be repeating every time we build an inline code span — there could be a lot of those on the screen.
The immediate solution for that problem is to move this to a final
static, i.e. a final
variable at the top level of the file (just like _kCodeStyle
and errorCodeStyle
are after these changes). That way it'll be computed lazily, once.
That won't support passing it a BuildContext so it can check MediaQueryData.boldText. I think the way I'd like to handle that is to have the result of this whole computation (the TextStyle we get at the end of it) to live on some InheritedWidget, so that at this spot we can just efficiently look it up. Then that InheritedWidget can live at the MessageList (or possibly higher in the tree) so that the computation doesn't get repeated frequently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I guess [Text.build] routinely does a [TextStyle.merge], to combine any style you pass it with the default text style. So that makes me feel a bit better that maybe that's cheap enough this is fine.
Still — these are spans, and there can potentially be a lot of those peppered through a paragraph, more densely than any normal UI is likely to have Text
widgets. And we want users to be able to scroll through tons of messages quickly and have that be nice and smooth, which puts a level of strain on fast widget building that isn't typical for most UIs. Plus there's some more stuff going on inside that weightVariableTextStyle
. So I think I'd like to stay paranoid about the performance here, and memoize this as a final
static for now, with memoizing on an InheritedWidget
as the plan for how to satisfy that TODO(a11y)
.
lib/widgets/text.dart
Outdated
assert(value >= 1 && value <= 1000); // https://fonts.google.com/variablefonts#axis-definitions | ||
|
||
return TextStyle( | ||
fontVariations: [FontVariation('wght', value.toDouble())], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a sign the parameters should just have type double
in the first place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From that page linked on the 1..1000
assert above—https://fonts.google.com/variablefonts#axis-definitions —it looks like the values on some axes are expected to "step" by less than one, making double
necessary for those, but for wght
, values are expected to step by one. That was my thinking behind using int
, but double
wouldn't be wrong, I suppose; it'd just let us pass around non-integer values that would get rounded, or floored, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, yeah.
Given that the underlying API takes double
, though, I think it makes sense to have this take double
, in order to remain as thin a layer as it can while doing its job.
test/widgets/text_test.dart
Outdated
Future<void> runCheck( | ||
String description, | ||
{ | ||
required TextStyle Function(BuildContext context) styleBuilder, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
Future<void> runCheck( | |
String description, | |
{ | |
required TextStyle Function(BuildContext context) styleBuilder, | |
Future<void> runCheck( | |
String description, { | |
required TextStyle Function(BuildContext context) styleBuilder, |
For some examples in the Flutter repo, see the Text
constructors.
test/widgets/text_test.dart
Outdated
} | ||
) async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
corresponding nit:
} | |
) async { | |
}) async { |
test/widgets/text_test.dart
Outdated
}); | ||
} | ||
|
||
runCheck('no context passed; default wght values', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: the name says "run", but this function's actual behavior is to define a test (by calling testWidgets
), not to run anything. (And if it did run anything, that'd be bad because it's not inside a test
or testWidgets
callback; whatever it does will happen unconditionally even when try to run only specific tests with e.g. flutter test --name
.)
Let's give it a name starting with "test", following the pattern set by testWidgets
itself. Perhaps testWeights
?
test/widgets/text_test.dart
Outdated
check(style) | ||
.isNotNull() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: indentation
test/widgets/text_test.dart
Outdated
..has((style) => style.inherit, 'inherit').isTrue() | ||
..has((style) => style.fontVariations, 'fontVariations') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these would be cleaner with a little extension, outside the test code itself. It's only a tiny bit longer that way in total, and it keeps the noise out of this test code (making the logic easier to see) while keeping the boilerplate concentrated to be maximally repetitive and boring (which is good for spotting errors in it, compared with having it interspersed with non-repetitive other code as here.)
In particular, the isTrue
here blends in pretty invisibly, whereas a line ..inherit.isTrue()
would be quite clear.
It'd be fine for such an extension to live in the individual test file (down at the bottom), when not expecting to reuse it. For this one, perhaps better yet would be to inaugurate a test/flutter_checks.dart
, similar to the stdlib_checks.dart
I added in #112.
91cd5bd
to
e217dce
Compare
Thanks for the review! Revision pushed. |
e217dce
to
12ab5d6
Compare
(Rebased to resolve conflicts with #115.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! All looks good, modulo small style comments below. Let's merge this after #101.
test/flutter_checks.dart
Outdated
Subject<List<FontVariation>?> get fontVariations => | ||
has((t) => t.fontVariations, 'fontVariations'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: better to make this consistent with the similar lines around it:
Subject<List<FontVariation>?> get fontVariations => | |
has((t) => t.fontVariations, 'fontVariations'); | |
Subject<List<FontVariation>?> get fontVariations => has((t) => t.fontVariations, 'fontVariations'); |
It makes the line somewhat long, but that's OK; the code is super boring, so the reader should just be skimming anyway to confirm the boringness. And keeping the format as boring as the code helps keep it easy to skim.
See also the Flutter style guide's discussion of this:
https://github.com./flutter/flutter/wiki/Style-guide-for-Flutter-repo#prefer-a-maximum-line-length-of-80-characters
Aim for a maximum line length of roughly 80 characters, but prefer going over if breaking the line would make it less readable, or if it would make the line less consistent with other nearby lines.
test/widgets/text_test.dart
Outdated
required TextStyle Function(BuildContext context) styleBuilder, | ||
bool platformRequestsBold = false, | ||
required List<FontVariation> expectedFontVariations, | ||
required FontWeight expectedFontWeight |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: always a trailing comma before newline:
required FontWeight expectedFontWeight | |
required FontWeight expectedFontWeight, |
test/widgets/text_test.dart
Outdated
test('clampVariableFontWeight', () { | ||
// Implementation assumes specific FontWeight values; | ||
// adapt if these change in a new Flutter version. | ||
check(FontWeight.values).deepEquals([ | ||
FontWeight.w100, FontWeight.w200, FontWeight.w300, | ||
FontWeight.w400, FontWeight.w500, FontWeight.w600, | ||
FontWeight.w700, FontWeight.w800, FontWeight.w900]); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cleanest to give this its own test
call with its own name. E.g.:
test('clampVariableFontWeight', () { | |
// Implementation assumes specific FontWeight values; | |
// adapt if these change in a new Flutter version. | |
check(FontWeight.values).deepEquals([ | |
FontWeight.w100, FontWeight.w200, FontWeight.w300, | |
FontWeight.w400, FontWeight.w500, FontWeight.w600, | |
FontWeight.w700, FontWeight.w800, FontWeight.w900]); | |
test('clampVariableFontWeight: FontWeight has the assumed list of values', () { | |
// Implementation assumes specific FontWeight values; | |
// adapt if these change in a new Flutter version. | |
check(FontWeight.values).deepEquals([ | |
FontWeight.w100, FontWeight.w200, FontWeight.w300, | |
FontWeight.w400, FontWeight.w500, FontWeight.w600, | |
FontWeight.w700, FontWeight.w800, FontWeight.w900, | |
]); | |
}); | |
test('clampVariableFontWeight', () { |
12ab5d6
to
9a9b234
Compare
Thanks for the review! Revision pushed.
Sure, SGTM. |
Like in the other places we ask for a monospace font. (The exact choice of font isn't actually important here; the text is just for displaying what message-content elements we haven't implemented yet. But we're about to define a TextStyle constant to encapsulate reasonable font-family fallbacks for a monospace font, and it'll be convenient to use that constant here too. It'll use "Source Code Pro" as its first choice.)
To encapsulate handling the odd bug where Apple's font collection doesn't respond to 'monospace'; see implementation comment.
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.
Assets and license downloaded from https://github.com./adobe-fonts/source-code-pro/tree/d3f1a5962 , so it's version 2.042 for upright, and version 1.062 for italic. We'll probably want to bundle more fonts into the app, like Source Sans 3, which we use for most text in Zulip web. But now at least we can see the process works! :) Fixes: zulip#64
Thanks! Looks good; merging. |
9a9b234
to
9e312b7
Compare
Fixes: #64
Related: #65