From bb73892c18583cf1bcd2a23f745cf66ee00ce2b2 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:27:56 -0500 Subject: [PATCH] fix: add custom token underline widget to make all text underlines equal height (#5170) --- lib/pages/chat/events/html_message.dart | 76 ++++++++----------- .../activity_vocab_widget.dart | 22 +++--- .../stt_transcript_tokens.dart | 25 +++--- .../token_rendering_util.dart | 43 ++--------- .../underline_text_widget.dart | 53 +++++++++++++ 5 files changed, 109 insertions(+), 110 deletions(-) create mode 100644 lib/pangea/toolbar/reading_assistance/underline_text_widget.dart diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index 9cddf5570..dda308453 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -20,6 +20,7 @@ import 'package:fluffychat/pangea/toolbar/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/reading_assistance/token_emoji_button.dart'; import 'package:fluffychat/pangea/toolbar/reading_assistance/token_rendering_util.dart'; import 'package:fluffychat/pangea/toolbar/reading_assistance/tokens_util.dart'; +import 'package:fluffychat/pangea/toolbar/reading_assistance/underline_text_widget.dart'; import 'package:fluffychat/utils/event_checkbox_extension.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; @@ -395,17 +396,6 @@ class HtmlMessage extends StatelessWidget { if (!allowedHtmlTags.contains(node.localName)) return const TextSpan(); // #Pangea - final renderer = TokenRenderingUtil( - existingStyle: pangeaMessageEvent != null - ? textStyle.merge( - AppConfig.messageTextStyle( - pangeaMessageEvent!.event, - textColor, - ), - ) - : textStyle, - ); - double fontSize = this.fontSize; if (readingAssistanceMode == ReadingAssistanceMode.practiceMode) { fontSize = (overlayController != null && overlayController!.maxWidth > 600 @@ -414,6 +404,19 @@ class HtmlMessage extends StatelessWidget { this.fontSize; } + final existingStyle = pangeaMessageEvent != null + ? textStyle + .merge( + AppConfig.messageTextStyle( + pangeaMessageEvent!.event, + textColor, + ), + ) + .copyWith(fontSize: fontSize) + : textStyle.copyWith(fontSize: fontSize); + + final renderer = TokenRenderingUtil(); + final underlineColor = Theme.of(context).colorScheme.primary.withAlpha(200); final newTokens = @@ -443,7 +446,8 @@ class HtmlMessage extends StatelessWidget { final tokenWidth = renderer.tokenTextWidthForContainer( node.text, Theme.of(context).colorScheme.primary.withAlpha(200), - fontSize: fontSize, + existingStyle, + fontSize, ); return TextSpan( @@ -472,10 +476,7 @@ class HtmlMessage extends StatelessWidget { TokenPracticeButton( token: token, controller: overlayController!.practiceController, - textStyle: renderer.style( - fontSize: fontSize, - underlineColor: underlineColor, - ), + textStyle: existingStyle, width: tokenWidth, textColor: textColor, ), @@ -496,28 +497,19 @@ class HtmlMessage extends StatelessWidget { : null, child: HoverBuilder( builder: (context, hovered) { - return RichText( + return UnderlineText( + text: node.text.trim(), + style: existingStyle, + linkStyle: linkStyle, textDirection: pangeaMessageEvent?.textDirection, - text: TextSpan( - children: [ - LinkifySpan( - text: node.text.trim(), - style: renderer.style( - fontSize: fontSize, - underlineColor: underlineColor, - selected: selected, - highlighted: highlighted, - isNew: isNew, - practiceMode: readingAssistanceMode == - ReadingAssistanceMode.practiceMode, - hovered: hovered, - ), - linkStyle: linkStyle, - onOpen: (url) => - UrlLauncher(context, url.url) - .launchUrl(), - ), - ], + underlineColor: TokenRenderingUtil.underlineColor( + underlineColor, + selected: selected, + highlighted: highlighted, + isNew: isNew, + practiceMode: readingAssistanceMode == + ReadingAssistanceMode.practiceMode, + hovered: hovered, ), ); }, @@ -669,10 +661,7 @@ class HtmlMessage extends StatelessWidget { // const TextSpan(text: '• '), TextSpan( text: '• ', - style: renderer.style( - underlineColor: underlineColor, - fontSize: fontSize, - ), + style: existingStyle, ), // Pangea# if (node.parent?.localName == 'ol') @@ -681,10 +670,7 @@ class HtmlMessage extends StatelessWidget { '${(node.parent?.nodes.whereType().toList().indexOf(node) ?? 0) + (int.tryParse(node.parent?.attributes['start'] ?? '1') ?? 1)}. ', // #Pangea // style: textStyle, - style: renderer.style( - underlineColor: underlineColor, - fontSize: fontSize, - ), + style: existingStyle, // Pangea# ), if (node.className == 'task-list-item') diff --git a/lib/pangea/activity_sessions/activity_session_chat/activity_vocab_widget.dart b/lib/pangea/activity_sessions/activity_session_chat/activity_vocab_widget.dart index bd78b0538..a7fe9e7f6 100644 --- a/lib/pangea/activity_sessions/activity_session_chat/activity_vocab_widget.dart +++ b/lib/pangea/activity_sessions/activity_session_chat/activity_vocab_widget.dart @@ -8,6 +8,7 @@ import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; import 'package:fluffychat/pangea/toolbar/reading_assistance/token_rendering_util.dart'; import 'package:fluffychat/pangea/toolbar/reading_assistance/tokens_util.dart'; +import 'package:fluffychat/pangea/toolbar/reading_assistance/underline_text_widget.dart'; import 'package:fluffychat/pangea/toolbar/token_rendering_mixin.dart'; import 'package:fluffychat/pangea/toolbar/word_card/word_zoom_widget.dart'; import 'package:fluffychat/widgets/hover_builder.dart'; @@ -143,12 +144,6 @@ class _VocabChipsState extends State<_VocabChips> with TokenRenderingMixin { tokens, widget.activityLangCode, ); - final renderer = TokenRenderingUtil( - existingStyle: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - fontSize: 14.0, - ), - ); return Wrap( spacing: 4.0, @@ -186,13 +181,14 @@ class _VocabChipsState extends State<_VocabChips> with TokenRenderingMixin { color: color, borderRadius: BorderRadius.circular(20), ), - child: Text( - v.lemma, - style: renderer.style( - underlineColor: Theme.of(context) - .colorScheme - .primary - .withAlpha(200), + child: UnderlineText( + text: v.lemma, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 14.0, + ), + underlineColor: TokenRenderingUtil.underlineColor( + Theme.of(context).colorScheme.primary.withAlpha(200), isNew: isNew, selected: _selectedVocab == v, hovered: hovered, diff --git a/lib/pangea/toolbar/reading_assistance/stt_transcript_tokens.dart b/lib/pangea/toolbar/reading_assistance/stt_transcript_tokens.dart index 36be582af..2a25706b9 100644 --- a/lib/pangea/toolbar/reading_assistance/stt_transcript_tokens.dart +++ b/lib/pangea/toolbar/reading_assistance/stt_transcript_tokens.dart @@ -4,6 +4,7 @@ import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/speech_to_text/speech_to_text_response_model.dart'; import 'package:fluffychat/pangea/toolbar/reading_assistance/token_rendering_util.dart'; import 'package:fluffychat/pangea/toolbar/reading_assistance/tokens_util.dart'; +import 'package:fluffychat/pangea/toolbar/reading_assistance/underline_text_widget.dart'; import 'package:fluffychat/widgets/hover_builder.dart'; class SttTranscriptTokens extends StatelessWidget { @@ -38,10 +39,6 @@ class SttTranscriptTokens extends StatelessWidget { } final messageCharacters = model.transcript.text.characters; - final renderer = TokenRenderingUtil( - existingStyle: (style ?? DefaultTextStyle.of(context).style), - ); - final newTokens = TokensUtil.getNewTokens( eventId, tokens, @@ -76,18 +73,14 @@ class SttTranscriptTokens extends StatelessWidget { child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: onClick != null ? () => onClick?.call(token) : null, - child: RichText( - text: TextSpan( - text: text, - style: renderer.style( - underlineColor: Theme.of(context) - .colorScheme - .primary - .withAlpha(200), - hovered: hovered, - selected: selected, - isNew: newTokens.any((t) => t == token.text), - ), + child: UnderlineText( + text: text, + style: style ?? DefaultTextStyle.of(context).style, + underlineColor: TokenRenderingUtil.underlineColor( + Theme.of(context).colorScheme.primary.withAlpha(200), + selected: selected, + hovered: hovered, + isNew: newTokens.any((t) => t == token.text), ), ), ), diff --git a/lib/pangea/toolbar/reading_assistance/token_rendering_util.dart b/lib/pangea/toolbar/reading_assistance/token_rendering_util.dart index 2b51b0ff6..6891fe345 100644 --- a/lib/pangea/toolbar/reading_assistance/token_rendering_util.dart +++ b/lib/pangea/toolbar/reading_assistance/token_rendering_util.dart @@ -3,42 +3,16 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/config/app_config.dart'; class TokenRenderingUtil { - final TextStyle existingStyle; - - TokenRenderingUtil({ - required this.existingStyle, - }); + TokenRenderingUtil(); static final Map _tokensWidthCache = {}; - TextStyle style({ - required Color underlineColor, - double? fontSize, - bool selected = false, - bool highlighted = false, - bool isNew = false, - bool practiceMode = false, - bool hovered = false, - }) => - existingStyle.copyWith( - fontSize: fontSize, - decoration: TextDecoration.underline, - decorationThickness: 4, - decorationColor: _underlineColor( - underlineColor, - selected: selected, - highlighted: highlighted, - isNew: isNew, - practiceMode: practiceMode, - hovered: hovered, - ), - ); - double tokenTextWidthForContainer( String text, - Color underlineColor, { - double? fontSize, - }) { + Color underlineColor, + TextStyle style, + double fontSize, + ) { final tokenSizeKey = "$text-$fontSize"; if (_tokensWidthCache.containsKey(tokenSizeKey)) { return _tokensWidthCache[tokenSizeKey]!; @@ -47,10 +21,7 @@ class TokenRenderingUtil { final textPainter = TextPainter( text: TextSpan( text: text, - style: style( - underlineColor: underlineColor, - fontSize: fontSize, - ), + style: style, ), maxLines: 1, textDirection: TextDirection.ltr, @@ -62,7 +33,7 @@ class TokenRenderingUtil { return width; } - Color _underlineColor( + static Color underlineColor( Color underlineColor, { bool selected = false, bool highlighted = false, diff --git a/lib/pangea/toolbar/reading_assistance/underline_text_widget.dart b/lib/pangea/toolbar/reading_assistance/underline_text_widget.dart new file mode 100644 index 000000000..b64c4e410 --- /dev/null +++ b/lib/pangea/toolbar/reading_assistance/underline_text_widget.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_linkify/flutter_linkify.dart'; + +import 'package:fluffychat/utils/url_launcher.dart'; + +class UnderlineText extends StatelessWidget { + final String text; + final TextStyle style; + final TextStyle? linkStyle; + final TextDirection? textDirection; + final Color? underlineColor; + + const UnderlineText({ + super.key, + required this.text, + required this.style, + this.linkStyle, + this.textDirection, + this.underlineColor, + }); + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.bottomLeft, + children: [ + RichText( + textDirection: textDirection, + text: TextSpan( + children: [ + LinkifySpan( + text: text, + style: style, + linkStyle: linkStyle, + onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), + ), + ], + ), + ), + Positioned( + bottom: 2, // fixed distance from baseline + left: 0, + right: 0, + child: Container( + height: 3, + color: underlineColor ?? Colors.transparent, + ), + ), + ], + ); + } +}