From 4d50910b8ade3251945a3f21944449946282a2b4 Mon Sep 17 00:00:00 2001 From: Moshe Dicker <75931499+dickermoshe@users.noreply.github.com> Date: Fri, 21 Feb 2025 13:40:05 -0500 Subject: [PATCH] Adds support for applying delta/factor transformations for TextTheme height, letter and word spacing (#158103) Closes https://github.com/flutter/flutter/issues/158102 The text theme has the `apply` method which does bulk operations on multiple text styles. It supports delta/factor ajustements for font size. This is very helpful for changing all the font sizes at once using a ratio or a simple delta. This PR add support for height, letter spacing and and word spacing too. ### Why is this so useful? Adjusting these in bulk is really helpful for using custom fonts. The Material font comes which its own default text styes and they're usually great. But many times they need to be nudged tighter. ### Doc Comment `apply` has no doc comments for `fontSizeFactor`/`fontSizeDelta` so i did not add any for the new `letterSpacingFactor`, `letterSpacingDelta`... either. If we want to add it, I'll do it. ### Tests Done! ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Tong Mu --- .../flutter/lib/src/material/text_theme.dart | 96 +++++++++++ .../test/material/text_theme_test.dart | 149 ++++++++++++++++++ 2 files changed, 245 insertions(+) diff --git a/packages/flutter/lib/src/material/text_theme.dart b/packages/flutter/lib/src/material/text_theme.dart index 3de2890553..2db569c3ff 100644 --- a/packages/flutter/lib/src/material/text_theme.dart +++ b/packages/flutter/lib/src/material/text_theme.dart @@ -396,6 +396,12 @@ class TextTheme with Diagnosticable { String? package, double fontSizeFactor = 1.0, double fontSizeDelta = 0.0, + double letterSpacingFactor = 1.0, + double letterSpacingDelta = 0.0, + double wordSpacingFactor = 1.0, + double wordSpacingDelta = 0.0, + double heightFactor = 1.0, + double heightDelta = 0.0, Color? displayColor, Color? bodyColor, TextDecoration? decoration, @@ -412,6 +418,12 @@ class TextTheme with Diagnosticable { fontFamilyFallback: fontFamilyFallback, fontSizeFactor: fontSizeFactor, fontSizeDelta: fontSizeDelta, + letterSpacingDelta: letterSpacingDelta, + letterSpacingFactor: letterSpacingFactor, + wordSpacingDelta: wordSpacingDelta, + wordSpacingFactor: wordSpacingFactor, + heightFactor: heightFactor, + heightDelta: heightDelta, package: package, ), displayMedium: displayMedium?.apply( @@ -423,6 +435,12 @@ class TextTheme with Diagnosticable { fontFamilyFallback: fontFamilyFallback, fontSizeFactor: fontSizeFactor, fontSizeDelta: fontSizeDelta, + letterSpacingDelta: letterSpacingDelta, + letterSpacingFactor: letterSpacingFactor, + wordSpacingDelta: wordSpacingDelta, + wordSpacingFactor: wordSpacingFactor, + heightFactor: heightFactor, + heightDelta: heightDelta, package: package, ), displaySmall: displaySmall?.apply( @@ -434,6 +452,12 @@ class TextTheme with Diagnosticable { fontFamilyFallback: fontFamilyFallback, fontSizeFactor: fontSizeFactor, fontSizeDelta: fontSizeDelta, + letterSpacingDelta: letterSpacingDelta, + letterSpacingFactor: letterSpacingFactor, + wordSpacingDelta: wordSpacingDelta, + wordSpacingFactor: wordSpacingFactor, + heightFactor: heightFactor, + heightDelta: heightDelta, package: package, ), headlineLarge: headlineLarge?.apply( @@ -445,6 +469,12 @@ class TextTheme with Diagnosticable { fontFamilyFallback: fontFamilyFallback, fontSizeFactor: fontSizeFactor, fontSizeDelta: fontSizeDelta, + letterSpacingDelta: letterSpacingDelta, + letterSpacingFactor: letterSpacingFactor, + wordSpacingDelta: wordSpacingDelta, + wordSpacingFactor: wordSpacingFactor, + heightFactor: heightFactor, + heightDelta: heightDelta, package: package, ), headlineMedium: headlineMedium?.apply( @@ -456,6 +486,12 @@ class TextTheme with Diagnosticable { fontFamilyFallback: fontFamilyFallback, fontSizeFactor: fontSizeFactor, fontSizeDelta: fontSizeDelta, + letterSpacingDelta: letterSpacingDelta, + letterSpacingFactor: letterSpacingFactor, + wordSpacingDelta: wordSpacingDelta, + wordSpacingFactor: wordSpacingFactor, + heightFactor: heightFactor, + heightDelta: heightDelta, package: package, ), headlineSmall: headlineSmall?.apply( @@ -467,6 +503,12 @@ class TextTheme with Diagnosticable { fontFamilyFallback: fontFamilyFallback, fontSizeFactor: fontSizeFactor, fontSizeDelta: fontSizeDelta, + letterSpacingDelta: letterSpacingDelta, + letterSpacingFactor: letterSpacingFactor, + wordSpacingDelta: wordSpacingDelta, + wordSpacingFactor: wordSpacingFactor, + heightFactor: heightFactor, + heightDelta: heightDelta, package: package, ), titleLarge: titleLarge?.apply( @@ -478,6 +520,12 @@ class TextTheme with Diagnosticable { fontFamilyFallback: fontFamilyFallback, fontSizeFactor: fontSizeFactor, fontSizeDelta: fontSizeDelta, + letterSpacingDelta: letterSpacingDelta, + letterSpacingFactor: letterSpacingFactor, + wordSpacingDelta: wordSpacingDelta, + wordSpacingFactor: wordSpacingFactor, + heightFactor: heightFactor, + heightDelta: heightDelta, package: package, ), titleMedium: titleMedium?.apply( @@ -489,6 +537,12 @@ class TextTheme with Diagnosticable { fontFamilyFallback: fontFamilyFallback, fontSizeFactor: fontSizeFactor, fontSizeDelta: fontSizeDelta, + letterSpacingDelta: letterSpacingDelta, + letterSpacingFactor: letterSpacingFactor, + wordSpacingDelta: wordSpacingDelta, + wordSpacingFactor: wordSpacingFactor, + heightFactor: heightFactor, + heightDelta: heightDelta, package: package, ), titleSmall: titleSmall?.apply( @@ -500,6 +554,12 @@ class TextTheme with Diagnosticable { fontFamilyFallback: fontFamilyFallback, fontSizeFactor: fontSizeFactor, fontSizeDelta: fontSizeDelta, + letterSpacingDelta: letterSpacingDelta, + letterSpacingFactor: letterSpacingFactor, + wordSpacingDelta: wordSpacingDelta, + wordSpacingFactor: wordSpacingFactor, + heightFactor: heightFactor, + heightDelta: heightDelta, package: package, ), bodyLarge: bodyLarge?.apply( @@ -511,6 +571,12 @@ class TextTheme with Diagnosticable { fontFamilyFallback: fontFamilyFallback, fontSizeFactor: fontSizeFactor, fontSizeDelta: fontSizeDelta, + letterSpacingDelta: letterSpacingDelta, + letterSpacingFactor: letterSpacingFactor, + wordSpacingDelta: wordSpacingDelta, + wordSpacingFactor: wordSpacingFactor, + heightFactor: heightFactor, + heightDelta: heightDelta, package: package, ), bodyMedium: bodyMedium?.apply( @@ -522,6 +588,12 @@ class TextTheme with Diagnosticable { fontFamilyFallback: fontFamilyFallback, fontSizeFactor: fontSizeFactor, fontSizeDelta: fontSizeDelta, + letterSpacingDelta: letterSpacingDelta, + letterSpacingFactor: letterSpacingFactor, + wordSpacingDelta: wordSpacingDelta, + wordSpacingFactor: wordSpacingFactor, + heightFactor: heightFactor, + heightDelta: heightDelta, package: package, ), bodySmall: bodySmall?.apply( @@ -533,6 +605,12 @@ class TextTheme with Diagnosticable { fontFamilyFallback: fontFamilyFallback, fontSizeFactor: fontSizeFactor, fontSizeDelta: fontSizeDelta, + letterSpacingDelta: letterSpacingDelta, + letterSpacingFactor: letterSpacingFactor, + wordSpacingDelta: wordSpacingDelta, + wordSpacingFactor: wordSpacingFactor, + heightFactor: heightFactor, + heightDelta: heightDelta, package: package, ), labelLarge: labelLarge?.apply( @@ -544,6 +622,12 @@ class TextTheme with Diagnosticable { fontFamilyFallback: fontFamilyFallback, fontSizeFactor: fontSizeFactor, fontSizeDelta: fontSizeDelta, + letterSpacingDelta: letterSpacingDelta, + letterSpacingFactor: letterSpacingFactor, + wordSpacingDelta: wordSpacingDelta, + wordSpacingFactor: wordSpacingFactor, + heightFactor: heightFactor, + heightDelta: heightDelta, package: package, ), labelMedium: labelMedium?.apply( @@ -555,6 +639,12 @@ class TextTheme with Diagnosticable { fontFamilyFallback: fontFamilyFallback, fontSizeFactor: fontSizeFactor, fontSizeDelta: fontSizeDelta, + letterSpacingDelta: letterSpacingDelta, + letterSpacingFactor: letterSpacingFactor, + wordSpacingDelta: wordSpacingDelta, + wordSpacingFactor: wordSpacingFactor, + heightFactor: heightFactor, + heightDelta: heightDelta, package: package, ), labelSmall: labelSmall?.apply( @@ -566,6 +656,12 @@ class TextTheme with Diagnosticable { fontFamilyFallback: fontFamilyFallback, fontSizeFactor: fontSizeFactor, fontSizeDelta: fontSizeDelta, + letterSpacingDelta: letterSpacingDelta, + letterSpacingFactor: letterSpacingFactor, + wordSpacingDelta: wordSpacingDelta, + wordSpacingFactor: wordSpacingFactor, + heightFactor: heightFactor, + heightDelta: heightDelta, package: package, ), ); diff --git a/packages/flutter/test/material/text_theme_test.dart b/packages/flutter/test/material/text_theme_test.dart index 952393fa9a..78eee4ca17 100644 --- a/packages/flutter/test/material/text_theme_test.dart +++ b/packages/flutter/test/material/text_theme_test.dart @@ -171,6 +171,155 @@ void main() { expect(sizeTheme.labelSmall!.fontSize, baseTheme.labelSmall!.fontSize! * 2.0 + 5.0); }); + test('TextTheme apply letterSpacingFactor letterSpacingDelta', () { + final Typography typography = Typography.material2018(); + final TextTheme baseTheme = Typography.englishLike2018.merge(typography.black); + final TextTheme sizeTheme = baseTheme.apply(letterSpacingFactor: 2.0, letterSpacingDelta: 5.0); + + expect( + sizeTheme.displayLarge!.letterSpacing, + baseTheme.displayLarge!.letterSpacing! * 2.0 + 5.0, + ); + expect( + sizeTheme.displayMedium!.letterSpacing, + baseTheme.displayMedium!.letterSpacing! * 2.0 + 5.0, + ); + expect( + sizeTheme.displaySmall!.letterSpacing, + baseTheme.displaySmall!.letterSpacing! * 2.0 + 5.0, + ); + expect( + sizeTheme.headlineLarge!.letterSpacing, + baseTheme.headlineLarge!.letterSpacing! * 2.0 + 5.0, + ); + expect( + sizeTheme.headlineMedium!.letterSpacing, + baseTheme.headlineMedium!.letterSpacing! * 2.0 + 5.0, + ); + expect( + sizeTheme.headlineSmall!.letterSpacing, + baseTheme.headlineSmall!.letterSpacing! * 2.0 + 5.0, + ); + expect(sizeTheme.titleLarge!.letterSpacing, baseTheme.titleLarge!.letterSpacing! * 2.0 + 5.0); + expect(sizeTheme.titleMedium!.letterSpacing, baseTheme.titleMedium!.letterSpacing! * 2.0 + 5.0); + expect(sizeTheme.titleSmall!.letterSpacing, baseTheme.titleSmall!.letterSpacing! * 2.0 + 5.0); + expect(sizeTheme.bodyLarge!.letterSpacing, baseTheme.bodyLarge!.letterSpacing! * 2.0 + 5.0); + expect(sizeTheme.bodyMedium!.letterSpacing, baseTheme.bodyMedium!.letterSpacing! * 2.0 + 5.0); + expect(sizeTheme.bodySmall!.letterSpacing, baseTheme.bodySmall!.letterSpacing! * 2.0 + 5.0); + expect(sizeTheme.labelLarge!.letterSpacing, baseTheme.labelLarge!.letterSpacing! * 2.0 + 5.0); + expect(sizeTheme.labelMedium!.letterSpacing, baseTheme.labelMedium!.letterSpacing! * 2.0 + 5.0); + expect(sizeTheme.labelSmall!.letterSpacing, baseTheme.labelSmall!.letterSpacing! * 2.0 + 5.0); + }); + + test('TextTheme apply wordSpacingFactor wordSpacingDelta', () { + final Typography typography = Typography.material2018(); + final TextTheme baseTheme = Typography.englishLike2018.merge(typography.black); + final TextTheme baseThemeWithWordSpacing = baseTheme.copyWith( + displayLarge: baseTheme.displayLarge!.copyWith(wordSpacing: 1.0), + displayMedium: baseTheme.displayMedium!.copyWith(wordSpacing: 1.0), + displaySmall: baseTheme.displaySmall!.copyWith(wordSpacing: 1.0), + headlineLarge: baseTheme.headlineLarge!.copyWith(wordSpacing: 1.0), + headlineMedium: baseTheme.headlineMedium!.copyWith(wordSpacing: 1.0), + headlineSmall: baseTheme.headlineSmall!.copyWith(wordSpacing: 1.0), + titleLarge: baseTheme.titleLarge!.copyWith(wordSpacing: 1.0), + titleMedium: baseTheme.titleMedium!.copyWith(wordSpacing: 1.0), + titleSmall: baseTheme.titleSmall!.copyWith(wordSpacing: 1.0), + bodyLarge: baseTheme.bodyLarge!.copyWith(wordSpacing: 1.0), + bodyMedium: baseTheme.bodyMedium!.copyWith(wordSpacing: 1.0), + bodySmall: baseTheme.bodySmall!.copyWith(wordSpacing: 1.0), + labelLarge: baseTheme.labelLarge!.copyWith(wordSpacing: 1.0), + labelMedium: baseTheme.labelMedium!.copyWith(wordSpacing: 1.0), + labelSmall: baseTheme.labelSmall!.copyWith(wordSpacing: 1.0), + ); + final TextTheme sizeTheme = baseThemeWithWordSpacing.apply( + wordSpacingFactor: 2.0, + wordSpacingDelta: 5.0, + ); + + expect( + sizeTheme.displayLarge!.wordSpacing, + baseThemeWithWordSpacing.displayLarge!.wordSpacing! * 2.0 + 5.0, + ); + expect( + sizeTheme.displayMedium!.wordSpacing, + baseThemeWithWordSpacing.displayMedium!.wordSpacing! * 2.0 + 5.0, + ); + expect( + sizeTheme.displaySmall!.wordSpacing, + baseThemeWithWordSpacing.displaySmall!.wordSpacing! * 2.0 + 5.0, + ); + expect( + sizeTheme.headlineLarge!.wordSpacing, + baseThemeWithWordSpacing.headlineLarge!.wordSpacing! * 2.0 + 5.0, + ); + expect( + sizeTheme.headlineMedium!.wordSpacing, + baseThemeWithWordSpacing.headlineMedium!.wordSpacing! * 2.0 + 5.0, + ); + expect( + sizeTheme.headlineSmall!.wordSpacing, + baseThemeWithWordSpacing.headlineSmall!.wordSpacing! * 2.0 + 5.0, + ); + expect( + sizeTheme.titleLarge!.wordSpacing, + baseThemeWithWordSpacing.titleLarge!.wordSpacing! * 2.0 + 5.0, + ); + expect( + sizeTheme.titleMedium!.wordSpacing, + baseThemeWithWordSpacing.titleMedium!.wordSpacing! * 2.0 + 5.0, + ); + expect( + sizeTheme.titleSmall!.wordSpacing, + baseThemeWithWordSpacing.titleSmall!.wordSpacing! * 2.0 + 5.0, + ); + expect( + sizeTheme.bodyLarge!.wordSpacing, + baseThemeWithWordSpacing.bodyLarge!.wordSpacing! * 2.0 + 5.0, + ); + expect( + sizeTheme.bodyMedium!.wordSpacing, + baseThemeWithWordSpacing.bodyMedium!.wordSpacing! * 2.0 + 5.0, + ); + expect( + sizeTheme.bodySmall!.wordSpacing, + baseThemeWithWordSpacing.bodySmall!.wordSpacing! * 2.0 + 5.0, + ); + expect( + sizeTheme.labelLarge!.wordSpacing, + baseThemeWithWordSpacing.labelLarge!.wordSpacing! * 2.0 + 5.0, + ); + expect( + sizeTheme.labelMedium!.wordSpacing, + baseThemeWithWordSpacing.labelMedium!.wordSpacing! * 2.0 + 5.0, + ); + expect( + sizeTheme.labelSmall!.wordSpacing, + baseThemeWithWordSpacing.labelSmall!.wordSpacing! * 2.0 + 5.0, + ); + }); + + test('TextTheme apply heightFactor heightDelta', () { + final Typography typography = Typography.material2021(); + final TextTheme baseTheme = Typography.englishLike2021.merge(typography.black); + final TextTheme sizeTheme = baseTheme.apply(heightFactor: 2.0, heightDelta: 5.0); + + expect(sizeTheme.displayLarge!.height, baseTheme.displayLarge!.height! * 2.0 + 5.0); + expect(sizeTheme.displayMedium!.height, baseTheme.displayMedium!.height! * 2.0 + 5.0); + expect(sizeTheme.displaySmall!.height, baseTheme.displaySmall!.height! * 2.0 + 5.0); + expect(sizeTheme.headlineLarge!.height, baseTheme.headlineLarge!.height! * 2.0 + 5.0); + expect(sizeTheme.headlineMedium!.height, baseTheme.headlineMedium!.height! * 2.0 + 5.0); + expect(sizeTheme.headlineSmall!.height, baseTheme.headlineSmall!.height! * 2.0 + 5.0); + expect(sizeTheme.titleLarge!.height, baseTheme.titleLarge!.height! * 2.0 + 5.0); + expect(sizeTheme.titleMedium!.height, baseTheme.titleMedium!.height! * 2.0 + 5.0); + expect(sizeTheme.titleSmall!.height, baseTheme.titleSmall!.height! * 2.0 + 5.0); + expect(sizeTheme.bodyLarge!.height, baseTheme.bodyLarge!.height! * 2.0 + 5.0); + expect(sizeTheme.bodyMedium!.height, baseTheme.bodyMedium!.height! * 2.0 + 5.0); + expect(sizeTheme.bodySmall!.height, baseTheme.bodySmall!.height! * 2.0 + 5.0); + expect(sizeTheme.labelLarge!.height, baseTheme.labelLarge!.height! * 2.0 + 5.0); + expect(sizeTheme.labelMedium!.height, baseTheme.labelMedium!.height! * 2.0 + 5.0); + expect(sizeTheme.labelSmall!.height, baseTheme.labelSmall!.height! * 2.0 + 5.0); + }); + test('TextTheme lerp with second parameter null', () { final TextTheme theme = Typography.material2018().black; final TextTheme lerped = TextTheme.lerp(theme, null, 0.25);