From 597cc8a477e250fc035762294bad7d2f309b812a Mon Sep 17 00:00:00 2001 From: boxdot Date: Fri, 31 Jan 2025 12:07:38 +0100 Subject: [PATCH] fix: use font weight specified via font variant Fixes #315 --- .../add_members_screen.dart | 2 +- .../connection_details.dart | 4 +- .../conversation_screen.dart | 5 +- .../conversation_details/group_details.dart | 16 ++- .../member_details_screen.dart | 2 +- .../conversation_list_content.dart | 20 +-- .../conversation_list_header.dart | 8 +- .../create_conversation_view.dart | 5 +- .../developer/developer_settings_screen.dart | 38 ++--- app/lib/intro_screen.dart | 7 +- .../message_list/display_message_tile.dart | 7 +- app/lib/message_list/message_renderer.dart | 11 +- app/lib/message_list/text_message_tile.dart | 7 +- app/lib/message_list/tile_timestamp.dart | 4 +- app/lib/registration/server_choice.dart | 4 +- app/lib/registration/username_password.dart | 4 +- app/lib/theme/styles.dart | 94 ++++++------ app/lib/theme/theme.dart | 1 + app/lib/theme/theme_data.dart | 45 +++--- app/lib/theme/variable_font_weight.dart | 89 ++++++++++++ app/lib/user/user_settings_screen.dart | 2 - app/lib/widgets/user_avatar.dart | 3 +- .../goldens/conversation_screen.png | 4 +- .../goldens/conversation_screen_empty.png | 4 +- .../goldens/conversation_list.png | 4 +- .../goldens/conversation_list_content.png | 4 +- .../conversation_list_content_empty.png | 4 +- .../goldens/conversation_list_empty.png | 4 +- .../developer/goldens/change_user_screen.png | 4 +- .../goldens/developer_settings_screen.png | 4 +- app/test/goldens/home_screen_desktop.png | 4 +- .../goldens/home_screen_desktop_empty.png | 4 +- .../home_screen_desktop_no_conversation.png | 4 +- .../message_list/goldens/message_list.png | 4 +- .../theme/goldens/typography_font_styles.png | 3 + .../theme/goldens/typography_font_weights.png | 3 + app/test/theme/typography_test.dart | 134 ++++++++++++++++++ 37 files changed, 397 insertions(+), 169 deletions(-) create mode 100644 app/lib/theme/variable_font_weight.dart create mode 100644 app/test/theme/goldens/typography_font_styles.png create mode 100644 app/test/theme/goldens/typography_font_weights.png create mode 100644 app/test/theme/typography_test.dart diff --git a/app/lib/conversation_details/add_members_screen.dart b/app/lib/conversation_details/add_members_screen.dart index d0cad1df..415f3f67 100644 --- a/app/lib/conversation_details/add_members_screen.dart +++ b/app/lib/conversation_details/add_members_screen.dart @@ -66,7 +66,7 @@ class AddMembersScreenView extends StatelessWidget { ), title: Text( contact.userName, - style: labelStyle, + style: Theme.of(context).textTheme.labelMedium, overflow: TextOverflow.ellipsis, ), trailing: Checkbox( diff --git a/app/lib/conversation_details/connection_details.dart b/app/lib/conversation_details/connection_details.dart index 2d8cab4d..fc6bd1e7 100644 --- a/app/lib/conversation_details/connection_details.dart +++ b/app/lib/conversation_details/connection_details.dart @@ -37,11 +37,11 @@ class ConnectionDetails extends StatelessWidget { ), Text( conversation.title, - style: labelStyle, + style: Theme.of(context).textTheme.labelMedium, ), Text( conversation.conversationType.description, - style: labelStyle, + style: Theme.of(context).textTheme.labelMedium, ), ], ), diff --git a/app/lib/conversation_details/conversation_screen.dart b/app/lib/conversation_details/conversation_screen.dart index 3129daba..130566ac 100644 --- a/app/lib/conversation_details/conversation_screen.dart +++ b/app/lib/conversation_details/conversation_screen.dart @@ -57,7 +57,10 @@ class _EmptyConversationPane extends StatelessWidget { return Scaffold( body: Center( child: Text( - style: labelStyle.copyWith(color: colorDMB), + style: Theme.of(context) + .textTheme + .labelMedium + ?.copyWith(color: colorDMB), "Select a chat to start messaging", ), ), diff --git a/app/lib/conversation_details/group_details.dart b/app/lib/conversation_details/group_details.dart index 9247a447..0200a82b 100644 --- a/app/lib/conversation_details/group_details.dart +++ b/app/lib/conversation_details/group_details.dart @@ -51,10 +51,13 @@ class GroupDetails extends StatelessWidget { final bytes = await image?.readAsBytes(); conversationDetailsCubit.setConversationPicture(bytes: bytes); }), - Text(conversation.title, style: labelStyle), + Text( + conversation.title, + style: Theme.of(context).textTheme.labelMedium, + ), Text( conversation.conversationType.description, - style: labelStyle, + style: Theme.of(context).textTheme.labelMedium, ), Expanded( child: Container( @@ -64,9 +67,12 @@ class GroupDetails extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( "Members", - style: boldLabelStyle, + style: Theme.of(context) + .textTheme + .labelMedium + ?.merge(VariableFontWeight.bold), ), Expanded( child: ListView.builder( @@ -82,7 +88,7 @@ class GroupDetails extends StatelessWidget { ), title: Text( member, - style: labelStyle, + style: Theme.of(context).textTheme.labelMedium, overflow: TextOverflow.ellipsis, ), trailing: const Icon(Icons.more_horiz), diff --git a/app/lib/conversation_details/member_details_screen.dart b/app/lib/conversation_details/member_details_screen.dart index 8ecebaab..1959a628 100644 --- a/app/lib/conversation_details/member_details_screen.dart +++ b/app/lib/conversation_details/member_details_screen.dart @@ -60,7 +60,7 @@ class MemberDetailsScreen extends StatelessWidget { const SizedBox(height: _padding), Text( memberUsername, - style: labelStyle, + style: Theme.of(context).textTheme.labelMedium, ), const SizedBox(height: _padding), ], diff --git a/app/lib/conversation_list/conversation_list_content.dart b/app/lib/conversation_list/conversation_list_content.dart index 04e36dd4..205e8403 100644 --- a/app/lib/conversation_list/conversation_list_content.dart +++ b/app/lib/conversation_list/conversation_list_content.dart @@ -186,10 +186,10 @@ class _UnreadBadge extends StatelessWidget { child: Text( badgeText, style: const TextStyle( - color: Colors.white, - fontSize: 10, - fontVariations: variationSemiBold, - letterSpacing: 0), + color: Colors.white, + fontSize: 10, + letterSpacing: 0, + ).merge(VariableFontWeight.semiBold), ), ); } @@ -210,16 +210,14 @@ class _LastMessage extends StatelessWidget { final style = TextStyle( color: colorDMB, fontSize: isSmallScreen(context) ? 14 : 13, - fontVariations: variationRegular, - letterSpacing: -0.2, height: 1.2, ); final contentStyle = conversation.unreadMessages > 0 - ? style.copyWith(fontVariations: variationMedium) + ? style.merge(VariableFontWeight.medium) : style; - final senderStyle = style.copyWith(fontVariations: variationSemiBold); + final senderStyle = style.merge(VariableFontWeight.semiBold); final (sender, displayedLastMessage) = switch (lastMessage?.message) { UiMessage_Content(field0: final content) => ( @@ -263,8 +261,6 @@ class _LastUpdated extends StatelessWidget { style: const TextStyle( color: colorDMB, fontSize: 11, - fontVariations: variationRegular, - letterSpacing: -0.2, ), ), ); @@ -289,9 +285,7 @@ class _ConversationTitle extends StatelessWidget { style: const TextStyle( color: convListItemTextColor, fontSize: 14, - fontVariations: variationSemiBold, - letterSpacing: -0.2, - ), + ).merge(VariableFontWeight.semiBold), ), ); } diff --git a/app/lib/conversation_list/conversation_list_header.dart b/app/lib/conversation_list/conversation_list_header.dart index 09407804..a2fd26c4 100644 --- a/app/lib/conversation_list/conversation_list_header.dart +++ b/app/lib/conversation_list/conversation_list_header.dart @@ -100,10 +100,8 @@ class _UsernameSpace extends StatelessWidget { displayName ?? "", style: const TextStyle( color: colorDMB, - fontVariations: variationBold, fontSize: 13, - letterSpacing: -0.2, - ), + ).merge(VariableFontWeight.bold), ), const SizedBox(height: 5), Text( @@ -111,9 +109,7 @@ class _UsernameSpace extends StatelessWidget { style: const TextStyle( color: colorDMB, fontSize: 10, - fontVariations: variationMedium, - letterSpacing: -0.2, - ), + ).merge(VariableFontWeight.medium), overflow: TextOverflow.ellipsis, ), ], diff --git a/app/lib/conversation_list/create_conversation_view.dart b/app/lib/conversation_list/create_conversation_view.dart index 334b65c9..f941d779 100644 --- a/app/lib/conversation_list/create_conversation_view.dart +++ b/app/lib/conversation_list/create_conversation_view.dart @@ -51,7 +51,10 @@ class _CreateConversationViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 50), - Text(widget.prompt, style: labelStyle), + Text( + widget.prompt, + style: Theme.of(context).textTheme.labelMedium, + ), const SizedBox(height: 20), Form( autovalidateMode: AutovalidateMode.always, diff --git a/app/lib/developer/developer_settings_screen.dart b/app/lib/developer/developer_settings_screen.dart index 1215f8ac..54518d01 100644 --- a/app/lib/developer/developer_settings_screen.dart +++ b/app/lib/developer/developer_settings_screen.dart @@ -21,6 +21,8 @@ class DeveloperSettingsScreen extends StatefulWidget { _DeveloperSettingsScreenState(); } +final _titleFontWeight = VariableFontWeight.medium; + class _DeveloperSettingsScreenState extends State { String? deviceToken; @@ -69,8 +71,6 @@ class _DeveloperSettingsScreenState extends State { } } -const _titleFontWeight = FontWeight.w600; - class DeveloperSettingsScreenView extends StatelessWidget { const DeveloperSettingsScreenView({ required this.deviceToken, @@ -87,13 +87,13 @@ class DeveloperSettingsScreenView extends StatelessWidget { Widget build(BuildContext context) { final user = context.select((LoadableUserCubit cubit) => cubit.state.user); - return SafeArea( - child: Scaffold( - appBar: AppBar( - title: const Text('Developer Settings'), - leading: const AppBarBackButton(), - ), - body: Center( + return Scaffold( + appBar: AppBar( + title: const Text('Developer Settings'), + leading: const AppBarBackButton(), + ), + body: SafeArea( + child: Center( child: Container( constraints: isPointer() ? const BoxConstraints(maxWidth: 800) : null, @@ -103,7 +103,7 @@ class DeveloperSettingsScreenView extends StatelessWidget { titleTextStyle: Theme.of(context) .textTheme .bodyLarge! - .copyWith(fontWeight: _titleFontWeight), + .merge(_titleFontWeight), ), child: ListView( children: [ @@ -137,10 +137,13 @@ class DeveloperSettingsScreenView extends StatelessWidget { ListTile( title: Text( user.userName, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith( color: Colors.red, - fontWeight: _titleFontWeight, - ), + ) + .merge(_titleFontWeight), ), subtitle: Text("id: ${user.clientId}"), trailing: const Icon(Icons.delete), @@ -155,10 +158,13 @@ class DeveloperSettingsScreenView extends StatelessWidget { ListTile( title: Text( 'Erase All Databases', - style: Theme.of(context).textTheme.bodyLarge?.copyWith( + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith( color: Colors.red, - fontWeight: _titleFontWeight, - ), + ) + .merge(_titleFontWeight), ), trailing: const Icon(Icons.delete), onTap: () => _confirmDialog( diff --git a/app/lib/intro_screen.dart b/app/lib/intro_screen.dart index a32d6849..3b8ff3c5 100644 --- a/app/lib/intro_screen.dart +++ b/app/lib/intro_screen.dart @@ -35,9 +35,9 @@ class IntroScreen extends StatelessWidget { filterQuality: FilterQuality.high, color: Colors.grey[350], ), - const _GradientText( + _GradientText( "Prototype.", - gradient: LinearGradient( + gradient: const LinearGradient( colors: [ Color.fromARGB(255, 34, 163, 255), Color.fromARGB(255, 72, 23, 250) @@ -46,9 +46,8 @@ class IntroScreen extends StatelessWidget { ), style: TextStyle( fontSize: 36, - fontVariations: variationMedium, letterSpacing: -0.9, - ), + ).merge(VariableFontWeight.medium), ), // Text button that opens the developer settings screen TextButton( diff --git a/app/lib/message_list/display_message_tile.dart b/app/lib/message_list/display_message_tile.dart index 43d67d3d..98e30bbb 100644 --- a/app/lib/message_list/display_message_tile.dart +++ b/app/lib/message_list/display_message_tile.dart @@ -89,11 +89,9 @@ class SystemMessageContent extends StatelessWidget { message.message, style: TextStyle( color: Colors.grey[700], - fontVariations: variationBold, - letterSpacing: -0.02, fontSize: 10, height: 1.4, - ), + ).merge(VariableFontWeight.bold), ), ); } @@ -115,10 +113,9 @@ class ErrorMessageContent extends StatelessWidget { message.message, style: const TextStyle( color: Colors.red, - fontWeight: FontWeight.w200, fontSize: 10, height: 1.0, - ), + ).merge(VariableFontWeight.w200), ), ); } diff --git a/app/lib/message_list/message_renderer.dart b/app/lib/message_list/message_renderer.dart index b8e2832a..fd66cbba 100644 --- a/app/lib/message_list/message_renderer.dart +++ b/app/lib/message_list/message_renderer.dart @@ -36,11 +36,12 @@ TextSpan _styledTextSpan( child: SelectionContainer.disabled( child: Text( keyword, - style: style?.copyWith( - //color: colorDMB, - fontSize: 12, - fontVariations: variationSemiBold, - ), + style: style + ?.copyWith( + //color: colorDMB, + fontSize: 12, + ) + .merge(VariableFontWeight.semiBold), ), ), ), diff --git a/app/lib/message_list/text_message_tile.dart b/app/lib/message_list/text_message_tile.dart index 748f3d3f..45386368 100644 --- a/app/lib/message_list/text_message_tile.dart +++ b/app/lib/message_list/text_message_tile.dart @@ -104,9 +104,8 @@ class TextMessageTile extends StatelessWidget { style: TextStyle( color: colorGreyDark, fontSize: isLargeScreen(context) ? 10 : 11, - fontVariations: variationMedium, letterSpacing: -0.1, - ), + ).merge(VariableFontWeight.medium), ), ), ); @@ -165,10 +164,8 @@ class TextMessageTile extends StatelessWidget { isSender ? "You" : contentMessage.sender.split("@").firstOrNull ?? "", style: const TextStyle( color: colorDMB, - fontVariations: variationSemiBold, fontSize: 12, - letterSpacing: -0.2, - ), + ).merge(VariableFontWeight.semiBold), overflow: TextOverflow.ellipsis, ), ); diff --git a/app/lib/message_list/tile_timestamp.dart b/app/lib/message_list/tile_timestamp.dart index c7fb9e22..15401991 100644 --- a/app/lib/message_list/tile_timestamp.dart +++ b/app/lib/message_list/tile_timestamp.dart @@ -29,10 +29,8 @@ class TileTimestamp extends StatelessWidget { timeString(timestamp), style: const TextStyle( color: colorDMB, - fontWeight: FontWeight.w200, fontSize: 10, - letterSpacing: -0.2, - ), + ).merge(VariableFontWeight.w200), ), ), ); diff --git a/app/lib/registration/server_choice.dart b/app/lib/registration/server_choice.dart index 3d083066..8cd60f09 100644 --- a/app/lib/registration/server_choice.dart +++ b/app/lib/registration/server_choice.dart @@ -31,9 +31,9 @@ class ServerChoice extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - const Text( + Text( 'Choose a server where you want to create your account', - style: labelStyle, + style: Theme.of(context).textTheme.labelMedium, ), Form( autovalidateMode: AutovalidateMode.always, diff --git a/app/lib/registration/username_password.dart b/app/lib/registration/username_password.dart index fae4dba1..5dea2805 100644 --- a/app/lib/registration/username_password.dart +++ b/app/lib/registration/username_password.dart @@ -31,9 +31,9 @@ class UsernamePasswordChoice extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - const Text( + Text( 'Choose a username and password', - style: labelStyle, + style: Theme.of(context).textTheme.labelMedium, ), Form( autovalidateMode: AutovalidateMode.always, diff --git a/app/lib/theme/styles.dart b/app/lib/theme/styles.dart index e61fd9a0..8356b4d1 100644 --- a/app/lib/theme/styles.dart +++ b/app/lib/theme/styles.dart @@ -5,6 +5,8 @@ import 'package:flutter/material.dart'; import 'dart:io' show Platform; +import 'variable_font_weight.dart'; + // === Devices === bool isSmallScreen(BuildContext context) { @@ -52,38 +54,20 @@ const swatchColor = Color(0xFFC0C6CE); const activeButtonColor = colorDMB; const inactiveButtonColor = colorDMBSuperLight; -// === Text === - -const labelStyle = TextStyle( - fontFamily: fontFamily, - fontSize: 14, - fontVariations: variationRegular, - letterSpacing: -0.02, -); - -const boldLabelStyle = TextStyle( - fontFamily: fontFamily, - fontSize: 14, - fontVariations: variationSemiBold, - letterSpacing: -0.02, -); - // === Inputs === -const inputTextStyle = TextStyle( +final inputTextStyle = TextStyle( fontFamily: fontFamily, fontSize: 14, - fontVariations: variationRegular, -); +).merge(VariableFontWeight.w400); final inputDecoration = InputDecoration( border: InputBorder.none, hintStyle: const TextStyle( color: colorDMBLight, fontSize: 11, - fontWeight: FontWeight.w100, fontFamily: fontFamily, - ), + ).merge(VariableFontWeight.w100), focusedBorder: textInputBorder, enabledBorder: textInputBorder, errorBorder: textInputBorder, @@ -95,12 +79,14 @@ final inputDecoration = InputDecoration( InputDecoration messageComposerInputDecoration(BuildContext context) => InputDecoration( border: InputBorder.none, - hintStyle: TextStyle( - color: colorGrey, - fontSize: isLargeScreen(context) ? 12 : 14, - fontWeight: FontWeight.w400, - fontFamily: fontFamily, - ), + hintStyle: DefaultTextStyle.of(context) + .style + .copyWith( + color: colorGrey, + fontSize: isLargeScreen(context) ? 12 : 14, + fontFamily: fontFamily, + ) + .merge(VariableFontWeight.w400), focusedBorder: textInputBorder, enabledBorder: textInputBorder, errorBorder: textInputBorder, @@ -109,17 +95,20 @@ InputDecoration messageComposerInputDecoration(BuildContext context) => fillColor: Colors.white, ); -TextStyle messageTextStyle(BuildContext context, bool inverted) => TextStyle( - color: inverted ? Colors.white : Colors.black, - fontFamily: fontFamily, - fontVariations: - isLargeScreen(context) ? variationRegular : variationMedium, - letterSpacing: -0.05, - fontSize: isLargeScreen(context) ? 14 : 15, - // NOTE: When specifying line height, the text is rendered inconsistently on - // Linux and macOS (and therefore also on Android and iOS). For now, we use the default one. - // height: isLargeScreen(context) ? 1.5 : 1.3, - ); +TextStyle messageTextStyle(BuildContext context, bool inverted) => + DefaultTextStyle.of(context) + .style + .copyWith( + color: inverted ? Colors.white : Colors.black, + letterSpacing: -0.05, + fontSize: isLargeScreen(context) ? 14 : 15, + // NOTE: When specifying line height, the text is rendered inconsistently on + // Linux and macOS (and therefore also on Android and iOS). For now, we use the default one. + // height: isLargeScreen(context) ? 1.5 : 1.3, + ) + .merge(isLargeScreen(context) + ? VariableFontWeight.normal + : VariableFontWeight.medium); final textInputBorder = OutlineInputBorder( borderSide: const BorderSide( @@ -140,11 +129,10 @@ ButtonStyle textButtonStyle(BuildContext context) { splashFactory: NoSplash.splashFactory, padding: WidgetStateProperty.all(const EdgeInsets.all(20)), textStyle: WidgetStateProperty.all( - TextStyle( - fontVariations: variationSemiBold, - fontFamily: fontFamily, - fontSize: isSmallScreen(context) ? 16 : 14, - ), + DefaultTextStyle.of(context) + .style + .copyWith(fontSize: isSmallScreen(context) ? 16 : 14) + .merge(VariableFontWeight.semiBold), ), ); } @@ -160,11 +148,12 @@ ButtonStyle dynamicTextButtonStyle( splashFactory: NoSplash.splashFactory, padding: WidgetStateProperty.all(const EdgeInsets.all(20)), textStyle: WidgetStateProperty.all( - TextStyle( - fontVariations: isMain ? variationSemiBold : variationMedium, - fontFamily: fontFamily, - fontSize: isSmallScreen(context) ? 16 : 14, - ), + DefaultTextStyle.of(context) + .style + .copyWith(fontSize: isSmallScreen(context) ? 16 : 14) + .merge( + isMain ? VariableFontWeight.semiBold : VariableFontWeight.medium, + ), ), ); } @@ -200,11 +189,10 @@ ButtonStyle buttonStyle(BuildContext context, bool isActive) { ), ), textStyle: WidgetStateProperty.all( - TextStyle( - fontVariations: variationSemiBold, - fontFamily: fontFamily, - fontSize: isSmallScreen(context) ? 16 : 14, - ), + DefaultTextStyle.of(context) + .style + .copyWith(fontSize: isSmallScreen(context) ? 16 : 14) + .merge(VariableFontWeight.semiBold), ), ); } diff --git a/app/lib/theme/theme.dart b/app/lib/theme/theme.dart index 57fe1133..070a78aa 100644 --- a/app/lib/theme/theme.dart +++ b/app/lib/theme/theme.dart @@ -6,3 +6,4 @@ export 'spacings.dart'; export 'theme_data.dart'; export 'responsive_screen.dart'; export 'styles.dart'; +export 'variable_font_weight.dart'; diff --git a/app/lib/theme/theme_data.dart b/app/lib/theme/theme_data.dart index 6305a55f..cc69c11b 100644 --- a/app/lib/theme/theme_data.dart +++ b/app/lib/theme/theme_data.dart @@ -5,31 +5,44 @@ import 'package:flutter/material.dart'; import 'package:prototype/theme/theme.dart'; +const _defaultLetterSpacing = -0.2; + ThemeData themeData(BuildContext context) => ThemeData( appBarTheme: AppBarTheme( color: Colors.white, elevation: 0, iconTheme: const IconThemeData(color: Colors.black), surfaceTintColor: Colors.black, - titleTextStyle: boldLabelStyle.copyWith(color: Colors.black), + titleTextStyle: TextStyle( + fontFamily: fontFamily, + color: Colors.black, + letterSpacing: _defaultLetterSpacing, + ).merge(VariableFontWeight.bold), ), fontFamily: fontFamily, textTheme: TextTheme( - displayLarge: TextStyle(letterSpacing: -0.2), - displayMedium: TextStyle(letterSpacing: -0.2), - displaySmall: TextStyle(letterSpacing: -0.2), - headlineLarge: TextStyle(letterSpacing: -0.2), - headlineMedium: TextStyle(letterSpacing: -0.2), - headlineSmall: TextStyle(letterSpacing: -0.2), - titleLarge: TextStyle(letterSpacing: -0.2), - titleMedium: TextStyle(letterSpacing: -0.2), - titleSmall: TextStyle(letterSpacing: -0.2), - bodyLarge: TextStyle(letterSpacing: -0.2), - bodyMedium: TextStyle(letterSpacing: -0.2), - bodySmall: TextStyle(letterSpacing: -0.2), - labelLarge: TextStyle(letterSpacing: -0.2), - labelMedium: TextStyle(letterSpacing: -0.2), - labelSmall: TextStyle(letterSpacing: -0.2), + displayLarge: TextStyle(letterSpacing: _defaultLetterSpacing) + .merge(VariableFontWeight.w400), + displayMedium: TextStyle(letterSpacing: _defaultLetterSpacing), + displaySmall: TextStyle(letterSpacing: _defaultLetterSpacing), + headlineLarge: TextStyle(letterSpacing: _defaultLetterSpacing), + headlineMedium: TextStyle(letterSpacing: _defaultLetterSpacing), + headlineSmall: TextStyle(letterSpacing: _defaultLetterSpacing), + titleLarge: TextStyle(letterSpacing: _defaultLetterSpacing) + .merge(VariableFontWeight.w500), + titleMedium: TextStyle(letterSpacing: _defaultLetterSpacing) + .merge(VariableFontWeight.w500), + titleSmall: TextStyle(letterSpacing: _defaultLetterSpacing) + .merge(VariableFontWeight.w500), + bodyLarge: TextStyle(letterSpacing: _defaultLetterSpacing), + bodyMedium: TextStyle(letterSpacing: _defaultLetterSpacing), + bodySmall: TextStyle(letterSpacing: _defaultLetterSpacing), + labelLarge: TextStyle(letterSpacing: _defaultLetterSpacing) + .merge(VariableFontWeight.w500), + labelMedium: TextStyle(letterSpacing: _defaultLetterSpacing) + .merge(VariableFontWeight.w500), + labelSmall: TextStyle(letterSpacing: _defaultLetterSpacing) + .merge(VariableFontWeight.w500), ), canvasColor: Colors.white, cardColor: Colors.white, diff --git a/app/lib/theme/variable_font_weight.dart b/app/lib/theme/variable_font_weight.dart new file mode 100644 index 00000000..bbe69f7b --- /dev/null +++ b/app/lib/theme/variable_font_weight.dart @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2025 Phoenix R&D GmbH +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +import 'package:flutter/widgets.dart'; + +/// Font weight for variable fonts. +/// +/// Misnomer: Technically, this is a font style. +class VariableFontWeight { + /// Thin, the least thick. + static const TextStyle w100 = TextStyle( + fontWeight: FontWeight.w100, + fontVariations: [FontVariation('wght', 100.0)], + ); + + /// Extra-light. + static const TextStyle w200 = TextStyle( + fontWeight: FontWeight.w200, + fontVariations: [FontVariation('wght', 200.0)], + ); + + /// Light. + static const TextStyle w300 = TextStyle( + fontWeight: FontWeight.w300, + fontVariations: [FontVariation('wght', 300.0)], + ); + + /// Normal / regular / plain. + static const TextStyle w400 = TextStyle( + fontWeight: FontWeight.w400, + fontVariations: [FontVariation('wght', 400.0)], + ); + + /// Medium. + static const TextStyle w500 = TextStyle( + fontWeight: FontWeight.w500, + fontVariations: [FontVariation('wght', 500.0)], + ); + + /// Semi-bold. + static const TextStyle w600 = TextStyle( + fontWeight: FontWeight.w600, + fontVariations: [FontVariation('wght', 600.0)], + ); + + /// Bold. + static const TextStyle w700 = TextStyle( + fontWeight: FontWeight.w700, + fontVariations: [FontVariation('wght', 700.0)], + ); + + /// Extra-bold. + static const TextStyle w800 = TextStyle( + fontWeight: FontWeight.w800, + fontVariations: [FontVariation('wght', 800.0)], + ); + + /// Black, the most thick. + static const TextStyle w900 = TextStyle( + fontWeight: FontWeight.w900, + fontVariations: [FontVariation('wght', 900.0)], + ); + + /// The default font weight. + static const TextStyle normal = w400; + + /// Slightly heaver than normal. + static const TextStyle medium = w500; + + /// Heaver than normal. + static const TextStyle semiBold = w600; + + /// A commonly used font weight that is heavier than normal. + static const TextStyle bold = w700; + + /// A list of all the font weights. + static const List values = [ + w100, + w200, + w300, + w400, + w500, + w600, + w700, + w800, + w900 + ]; +} diff --git a/app/lib/user/user_settings_screen.dart b/app/lib/user/user_settings_screen.dart index 9d1e4b48..b64ed851 100644 --- a/app/lib/user/user_settings_screen.dart +++ b/app/lib/user/user_settings_screen.dart @@ -86,8 +86,6 @@ class _UserSettingsScreenState extends State { style: const TextStyle( color: colorDMB, fontSize: 12, - fontVariations: variationRegular, - letterSpacing: -0.2, ), ), ], diff --git a/app/lib/widgets/user_avatar.dart b/app/lib/widgets/user_avatar.dart index 424c0024..636d15ce 100644 --- a/app/lib/widgets/user_avatar.dart +++ b/app/lib/widgets/user_avatar.dart @@ -43,8 +43,7 @@ class UserAvatar extends StatelessWidget { style: TextStyle( color: Colors.white, fontSize: 10 * size / 24, - fontWeight: FontWeight.bold, - ), + ).merge(VariableFontWeight.bold), ), ), ), diff --git a/app/test/conversation/goldens/conversation_screen.png b/app/test/conversation/goldens/conversation_screen.png index f8884c83..91fc3045 100644 --- a/app/test/conversation/goldens/conversation_screen.png +++ b/app/test/conversation/goldens/conversation_screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b3b7a890e5b6266c41cd35b72084dcf32cbd93bc73dc71b4bda211f3a6eff8f -size 142821 +oid sha256:ca5c227f362ec7693387330ea1bafffd24cf69d94f662128c42ca5f92af52d4b +size 143987 diff --git a/app/test/conversation/goldens/conversation_screen_empty.png b/app/test/conversation/goldens/conversation_screen_empty.png index eb4b0ead..9ec77ef5 100644 --- a/app/test/conversation/goldens/conversation_screen_empty.png +++ b/app/test/conversation/goldens/conversation_screen_empty.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3542b1f47c58bc5f960b6ca43c3f7cba199cbe746be190f43a9fc8895f0376ae -size 27583 +oid sha256:6bfc0f9e3ad747aa5f9a000b7b94325296eba19e136e757733bc21b0e0f890f3 +size 25885 diff --git a/app/test/conversation_list/goldens/conversation_list.png b/app/test/conversation_list/goldens/conversation_list.png index 283fa78c..2e125015 100644 --- a/app/test/conversation_list/goldens/conversation_list.png +++ b/app/test/conversation_list/goldens/conversation_list.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b40f61e5c5bc6a9f5cbac3d7573cfcb1856b1fe157bebf54f7ef21178fd35e7 -size 176060 +oid sha256:f140b7cdcea2f82194409fc62e3e0f53d4db3c60a18a24588bd00e06ebe22a5b +size 171353 diff --git a/app/test/conversation_list/goldens/conversation_list_content.png b/app/test/conversation_list/goldens/conversation_list_content.png index b31f16db..58001b9a 100644 --- a/app/test/conversation_list/goldens/conversation_list_content.png +++ b/app/test/conversation_list/goldens/conversation_list_content.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:285a4ff8a75ca0130e29d8526a0e27b649a840b5f3f80b7d0c892b555dd98c93 -size 192855 +oid sha256:24d6dff1e366024082291b732ee5ef8c25b7d4a4cfe325b799537f6dce0f93df +size 199304 diff --git a/app/test/conversation_list/goldens/conversation_list_content_empty.png b/app/test/conversation_list/goldens/conversation_list_content_empty.png index fad7af21..f3315722 100644 --- a/app/test/conversation_list/goldens/conversation_list_content_empty.png +++ b/app/test/conversation_list/goldens/conversation_list_content_empty.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c5bf82dfb80e2a2852940b0b2112810be3ae12191efc5ddd75b314c98dfaa6f -size 27914 +oid sha256:1107ab566379d3a94d345edccb85465247ad59e0ef63f82f7760e9af78fd7370 +size 28784 diff --git a/app/test/conversation_list/goldens/conversation_list_empty.png b/app/test/conversation_list/goldens/conversation_list_empty.png index 84417250..a7ca380a 100644 --- a/app/test/conversation_list/goldens/conversation_list_empty.png +++ b/app/test/conversation_list/goldens/conversation_list_empty.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7814ee2c3958a91c0dcd90a140b9192054cffa5afca567ba0091f1c51984137c -size 55677 +oid sha256:af590eac5fe01c1ad54c9480f7a4574c5807abdb6bba2b66f3f8c088a9ca42bb +size 55409 diff --git a/app/test/developer/goldens/change_user_screen.png b/app/test/developer/goldens/change_user_screen.png index b832b947..6cbeb6b8 100644 --- a/app/test/developer/goldens/change_user_screen.png +++ b/app/test/developer/goldens/change_user_screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ed7c8b6596eef595544ba12cc48332033e5390ec068029b3599e45bd9f97e32 -size 143392 +oid sha256:5beea959177d29cc7e9baf8e64ea2f5fca1f42abc2d963c23001181be5616a09 +size 143632 diff --git a/app/test/developer/goldens/developer_settings_screen.png b/app/test/developer/goldens/developer_settings_screen.png index ea1fab6e..ae0b5c73 100644 --- a/app/test/developer/goldens/developer_settings_screen.png +++ b/app/test/developer/goldens/developer_settings_screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:863e1cc82ba41baf4ac451da87d5393856f72ef2d224c56a45e3e2d61953309f -size 139098 +oid sha256:206767654f329170c8ae74dd1920d30438f8b838dd370702a4617158f603ffad +size 135673 diff --git a/app/test/goldens/home_screen_desktop.png b/app/test/goldens/home_screen_desktop.png index 9bcce30a..25e723d5 100644 --- a/app/test/goldens/home_screen_desktop.png +++ b/app/test/goldens/home_screen_desktop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56c3f78070c9a66ea69da82f9a83729dc9c9bf6cc76e8b62b2a23f3a280efb33 -size 220362 +oid sha256:b7c8517f6126c9c3357e81defae10315ed87cab5ffea8317a090ce3ae45a80a3 +size 224362 diff --git a/app/test/goldens/home_screen_desktop_empty.png b/app/test/goldens/home_screen_desktop_empty.png index a92d56d7..ae7dcf7b 100644 --- a/app/test/goldens/home_screen_desktop_empty.png +++ b/app/test/goldens/home_screen_desktop_empty.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ecf12504d3a242f00174fd567041fa530e1a31c587fca35ddb8a49b2267223e8 -size 89236 +oid sha256:fc0b85595439579c3524b2ce913b6d3638bf4cc28b6fafa3daac192f4ebcc496 +size 86802 diff --git a/app/test/goldens/home_screen_desktop_no_conversation.png b/app/test/goldens/home_screen_desktop_no_conversation.png index aaff74b7..7cc998c9 100644 --- a/app/test/goldens/home_screen_desktop_no_conversation.png +++ b/app/test/goldens/home_screen_desktop_no_conversation.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8f43e3b21a6aec60869d69244c3e5b9fdba60fd0987bf5a6c0b980bd8d77380 -size 128846 +oid sha256:550b11ae41430b0f38876e289104a9fda7f1e34e542b5839568f11334bf99672 +size 124334 diff --git a/app/test/message_list/goldens/message_list.png b/app/test/message_list/goldens/message_list.png index 7a314374..4006a530 100644 --- a/app/test/message_list/goldens/message_list.png +++ b/app/test/message_list/goldens/message_list.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33ea90995c5a699b9462e51d85d9f54ea1a6a249b4c0966bf800da8560373b54 -size 131498 +oid sha256:5b4b549d440c607106b016fdeafb7f67387923c1fb3583544dcc8f9147efe0a3 +size 132342 diff --git a/app/test/theme/goldens/typography_font_styles.png b/app/test/theme/goldens/typography_font_styles.png new file mode 100644 index 00000000..ceb948ef --- /dev/null +++ b/app/test/theme/goldens/typography_font_styles.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80c88eb7e3548792792644538a5d1a5ba66b3436b85c4c05acac6aadd5058e55 +size 135961 diff --git a/app/test/theme/goldens/typography_font_weights.png b/app/test/theme/goldens/typography_font_weights.png new file mode 100644 index 00000000..649394e8 --- /dev/null +++ b/app/test/theme/goldens/typography_font_weights.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f529a30bce92bc3c32fefa063502249209b12d7b9f7ec5765bfa571cde10c0e0 +size 116669 diff --git a/app/test/theme/typography_test.dart b/app/test/theme/typography_test.dart new file mode 100644 index 00000000..13883ff5 --- /dev/null +++ b/app/test/theme/typography_test.dart @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: 2025 Phoenix R&D GmbH +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:prototype/theme/theme.dart'; + +void main() { + group('Typography', () { + Widget buildSubject(Widget widget) => Builder( + builder: (context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: themeData(context), + home: Scaffold( + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: Spacings.xs), + child: Center( + child: widget, + ), + ), + ), + ), + ); + }, + ); + + testWidgets('font styles', (tester) async { + await tester.pumpWidget( + buildSubject( + Builder(builder: (context) { + return Column( + children: [ + Text( + "Display Large", + style: Theme.of(context).textTheme.displayLarge, + ), + Text( + "Display Medium", + style: Theme.of(context).textTheme.displayMedium, + ), + Text( + "Display Small", + style: Theme.of(context).textTheme.displaySmall, + ), + Text( + "Headline Large", + style: Theme.of(context).textTheme.headlineLarge, + ), + Text( + "Headline Medium", + style: Theme.of(context).textTheme.headlineMedium, + ), + Text( + "Headline Small", + style: Theme.of(context).textTheme.headlineSmall, + ), + Text( + "Title Large", + style: Theme.of(context).textTheme.titleLarge, + ), + Text( + "Title Medium", + style: Theme.of(context).textTheme.titleMedium, + ), + Text( + "Title Small", + style: Theme.of(context).textTheme.titleSmall, + ), + Text( + "Body Large", + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + "Body Medium", + style: Theme.of(context).textTheme.bodyMedium, + ), + Text( + "Body Small", + style: Theme.of(context).textTheme.bodySmall, + ), + Text( + "Label Large", + style: Theme.of(context).textTheme.labelLarge, + ), + Text( + "Label Medium", + style: Theme.of(context).textTheme.labelMedium, + ), + Text( + "Label Small", + style: Theme.of(context).textTheme.labelSmall, + ), + ], + ); + }), + ), + ); + + await expectLater( + find.byType(MaterialApp), + matchesGoldenFile('goldens/typography_font_styles.png'), + ); + }); + + testWidgets('font weights', (tester) async { + await tester.pumpWidget( + buildSubject( + Builder(builder: (context) { + return Column( + children: FontWeight.values + .map( + (weight) => Text( + "This text has weight $weight", + style: TextStyle().merge( + VariableFontWeight.values[weight.index], + ), + ), + ) + .toList(), + ); + }), + ), + ); + + await expectLater( + find.byType(MaterialApp), + matchesGoldenFile('goldens/typography_font_weights.png'), + ); + }); + }); +}