Skip to content

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

Merged
merged 7 commits into from
May 31, 2023

Conversation

chrisbobbe
Copy link
Collaborator

Fixes: #64
Related: #65

@chrisbobbe chrisbobbe requested a review from gnprice May 18, 2023 00:34
@chrisbobbe
Copy link
Collaborator Author

Tested on iOS and Android, and the text appears in the new font and with an appropriate-looking weight.

@chrisbobbe chrisbobbe force-pushed the pr-bundle-source-code-pro-font branch from f6d744a to f62df19 Compare May 23, 2023 21:11
@chrisbobbe
Copy link
Collaborator Author

Just updated with a newer version of the font. As @andersk points out:

Google Fonts provides old versions of Source Code Pro (upright 2.030, italic 1.050), compared to the current versions from Adobe (upright 2.042, italic 1.062).

I also updated this to add an entry for this font's license in LicenseRegistry. For a proposed UI to view the list of licenses tracked by LicenseRegistry, see:

@chrisbobbe chrisbobbe force-pushed the pr-bundle-source-code-pro-font branch from f62df19 to 91cd5bd Compare May 23, 2023 21:25
Copy link
Member

@gnprice gnprice left a 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.

Comment on lines 96 to 97
for (final fontWeight in values) {
if (fontWeight.value > wght) {
Copy link
Member

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 {
  // …
}

Copy link
Collaborator Author

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.

// TODO(a11y) make `context` required when callers can adapt?
TextStyle weightVariableTextStyle(BuildContext? context, {
int? wght,
int? wghtPlatformRequestsBold,
Copy link
Member

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.

Comment on lines 52 to 53
int? wght,
int? wghtPlatformRequestsBold,
Copy link
Member

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));.

.merge(const TextStyle(
backgroundColor: Color(0xffeeeeee),
fontSize: 0.825 * kBaseFontSize))
.merge(weightVariableTextStyle(null)), // TODO(a11y) pass a BuildContext
Copy link
Member

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.

Copy link
Member

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).

assert(value >= 1 && value <= 1000); // https://fonts.google.com/variablefonts#axis-definitions

return TextStyle(
fontVariations: [FontVariation('wght', value.toDouble())],
Copy link
Member

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.

Copy link
Collaborator Author

@chrisbobbe chrisbobbe May 24, 2023

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.

Copy link
Member

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.

Comment on lines 10 to 13
Future<void> runCheck(
String description,
{
required TextStyle Function(BuildContext context) styleBuilder,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
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.

Comment on lines 17 to 18
}
) async {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

corresponding nit:

Suggested change
}
) async {
}) async {

});
}

runCheck('no context passed; default wght values',
Copy link
Member

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?

Comment on lines 27 to 28
check(style)
.isNotNull()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: indentation

Comment on lines 29 to 30
..has((style) => style.inherit, 'inherit').isTrue()
..has((style) => style.fontVariations, 'fontVariations')
Copy link
Member

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.

@chrisbobbe
Copy link
Collaborator Author

Thanks for the review! Revision pushed.

@chrisbobbe chrisbobbe force-pushed the pr-bundle-source-code-pro-font branch from e217dce to 12ab5d6 Compare May 26, 2023 23:42
@chrisbobbe
Copy link
Collaborator Author

(Rebased to resolve conflicts with #115.)

Copy link
Member

@gnprice gnprice left a 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.

Comment on lines 9 to 10
Subject<List<FontVariation>?> get fontVariations =>
has((t) => t.fontVariations, 'fontVariations');
Copy link
Member

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:

Suggested change
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.

required TextStyle Function(BuildContext context) styleBuilder,
bool platformRequestsBold = false,
required List<FontVariation> expectedFontVariations,
required FontWeight expectedFontWeight
Copy link
Member

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:

Suggested change
required FontWeight expectedFontWeight
required FontWeight expectedFontWeight,

See also:
https://github.com./flutter/flutter/wiki/Style-guide-for-Flutter-repo#use-a-trailing-comma-for-arguments-parameters-and-list-items-but-only-if-they-each-have-their-own-line

Comment on lines 66 to 75
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]);

Copy link
Member

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.:

Suggested change
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', () {

@chrisbobbe chrisbobbe force-pushed the pr-bundle-source-code-pro-font branch from 12ab5d6 to 9a9b234 Compare May 31, 2023 19:23
@chrisbobbe
Copy link
Collaborator Author

Thanks for the review! Revision pushed.

Let's merge this after #101.

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
@gnprice
Copy link
Member

gnprice commented May 31, 2023

Thanks! Looks good; merging.

@gnprice gnprice force-pushed the pr-bundle-source-code-pro-font branch from 9a9b234 to 9e312b7 Compare May 31, 2023 23:16
@gnprice gnprice merged commit 9e312b7 into zulip:main May 31, 2023
@chrisbobbe chrisbobbe deleted the pr-bundle-source-code-pro-font branch May 31, 2023 23:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Try bundling fonts
3 participants