Skip to content

text: Add and use proportionalLetterSpacing #629

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 4 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions lib/widgets/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,6 @@ class _ZulipAppState extends State<ZulipApp> with WidgetsBindingObserver {
seedColor: kZulipBrandColor,
),
scaffoldBackgroundColor: const Color(0xfff6f6f6),
// `preferBelow: false` seems like a better default for mobile;
// the area below a long-press target seems more likely to be hidden by
// a finger or thumb than the area above.
tooltipTheme: const TooltipThemeData(preferBelow: false),
);

Expand Down
6 changes: 3 additions & 3 deletions lib/widgets/message_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,7 @@ class StreamMessageRecipientHeader extends StatelessWidget {
final textStyle = TextStyle(
color: contrastingColor,
fontSize: 16,
letterSpacing: 0.02 * 16,
letterSpacing: proportionalLetterSpacing(context, 0.02, baseFontSize: 16),
height: (18 / 16),
).merge(weightVariableTextStyle(context, wght: 600));

Expand Down Expand Up @@ -775,9 +775,9 @@ class DmRecipientHeader extends StatelessWidget {
child: Icon(size: 16, ZulipIcons.user)),
Expanded(
child: Text(title,
style: const TextStyle(
style: TextStyle(
fontSize: 16,
letterSpacing: 0.02 * 16,
letterSpacing: proportionalLetterSpacing(context, 0.02, baseFontSize: 16),
height: (18 / 16),
).merge(weightVariableTextStyle(context, wght: 600)),
overflow: TextOverflow.ellipsis)),
Expand Down
2 changes: 1 addition & 1 deletion lib/widgets/subscription_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ class _SubscriptionListHeader extends StatelessWidget {
style: TextStyle(
color: const HSLColor.fromAHSL(1.0, 240, 0.1, 0.5).toColor(),
fontSize: 14,
letterSpacing: 0.04 * 14,
letterSpacing: proportionalLetterSpacing(context, 0.04, baseFontSize: 14),
height: (16 / 14),
))),
const SizedBox(width: 8),
Expand Down
19 changes: 19 additions & 0 deletions lib/widgets/text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,22 @@ FontWeight clampVariableFontWeight(double wght) {
/// font's own custom-defined "wght" axis. But it's a great guess,
/// at least without knowledge of the particular font.
double wghtFromFontWeight(FontWeight fontWeight) => fontWeight.value.toDouble();

/// A [TextStyle.letterSpacing] value from a given proportion of the font size.
///
/// Returns [baseFontSize] scaled by the ambient [MediaQueryData.textScaler],
/// multiplied by [proportion] (e.g., 0.01).
///
/// Using [MediaQueryData.textScaler] ensures that [proportion] is still
/// respected when the device font size setting is adjusted.
/// To opt out of this behavior, pass [TextScaler.noScaling] or some other value
/// for [textScaler].
double proportionalLetterSpacing(
BuildContext context,
double proportion, {
required double baseFontSize,
TextScaler? textScaler,
}) {
final effectiveTextScaler = textScaler ?? MediaQuery.textScalerOf(context);
return effectiveTextScaler.scale(baseFontSize) * proportion;
}
11 changes: 2 additions & 9 deletions test/widgets/emoji_reaction_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import '../example_data.dart' as eg;
import '../model/binding.dart';
import '../model/test_store.dart';
import '../test_images.dart';
import 'text_test.dart';

void main() {
TestZulipBinding.ensureInitialized();
Expand All @@ -36,14 +37,6 @@ void main() {
await fontLoader.load();
}

// From trying the options on an iPhone 13 Pro running iOS 16.6.1:
const textScaleFactors = <double>[
0.8235, // smallest
1,
1.3529, // largest without using the "Larger Accessibility Sizes" setting
3.1176, // largest
];

Future<void> setupChipsInBox(WidgetTester tester, {
required List<Reaction> reactions,
double? width,
Expand Down Expand Up @@ -76,7 +69,7 @@ void main() {
for (final displayEmojiReactionUsers in [true, false]) {
for (final emojiset in [Emojiset.text, Emojiset.google]) {
for (final textDirection in TextDirection.values) {
for (final textScaleFactor in textScaleFactors) {
for (final textScaleFactor in kTextScaleFactors) {
Future<void> runSmokeTest(
String description,
List<Reaction> reactions, {
Expand Down
62 changes: 61 additions & 1 deletion test/widgets/text_test.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@

import 'package:checks/checks.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:zulip/widgets/text.dart';

import '../flutter_checks.dart';

// From trying the options on an iPhone 13 Pro running iOS 16.6.1:
const kTextScaleFactors = <double>[
0.8235, // smallest
1,
1.3529, // largest without using the "Larger Accessibility Sizes" setting
3.1176, // largest
];

void main() {
group('zulipTypography', () {
Future<Typography> getZulipTypography(WidgetTester tester, {
Expand Down Expand Up @@ -207,4 +214,57 @@ void main() {
check(clampVariableFontWeight(999)) .equals(FontWeight.w900);
check(clampVariableFontWeight(1000)) .equals(FontWeight.w900);
});

group('proportionalLetterSpacing', () {
Future<void> testLetterSpacing(
String description, {
required double Function(BuildContext context) getValue,
double? ambientTextScaleFactor,
required double expected,
}) async {
testWidgets(description, (WidgetTester tester) async {
if (ambientTextScaleFactor != null) {
tester.platformDispatcher.textScaleFactorTestValue = ambientTextScaleFactor;
addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue);
}
await tester.pumpWidget(
MaterialApp(
home: Builder(builder: (context) => Text('',
style: TextStyle(letterSpacing: getValue(context))))));

final TextStyle? style = tester.widget<Text>(find.byType(Text)).style;
final actualLetterSpacing = style!.letterSpacing!;
check((actualLetterSpacing - expected).abs()).isLessThan(0.0001);
});
}

testLetterSpacing('smoke 1',
getValue: (context) => proportionalLetterSpacing(context, 0.01, baseFontSize: 14),
expected: 0.14);

testLetterSpacing('smoke 2',
getValue: (context) => proportionalLetterSpacing(context, 0.02, baseFontSize: 16),
expected: 0.32);

for (final textScaleFactor in kTextScaleFactors) {
testLetterSpacing('ambient text scale factor $textScaleFactor, no override',
ambientTextScaleFactor: textScaleFactor,
getValue: (context) => proportionalLetterSpacing(context, 0.01, baseFontSize: 14),
expected: 0.14 * textScaleFactor);

testLetterSpacing('ambient text scale factor $textScaleFactor, override with no scaling',
ambientTextScaleFactor: textScaleFactor,
getValue: (context) => proportionalLetterSpacing(context,
0.01, baseFontSize: 14, textScaler: TextScaler.noScaling),
expected: 0.14);

final clampingTextScaler = TextScaler.linear(textScaleFactor)
.clamp(minScaleFactor: 0.9, maxScaleFactor: 1.1);
testLetterSpacing('ambient text scale factor $textScaleFactor, override with clamping',
ambientTextScaleFactor: textScaleFactor,
getValue: (context) => proportionalLetterSpacing(context,
0.01, baseFontSize: 14, textScaler: clampingTextScaler),
expected: clampingTextScaler.scale(14) * 0.01);
}
});
}