fix: add custom token underline widget to make all text underlines equal height (#5170)

This commit is contained in:
ggurdin 2026-01-12 11:27:56 -05:00 committed by GitHub
parent ebe22129bc
commit bb73892c18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 109 additions and 110 deletions

View file

@ -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<dom.Element>().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')

View file

@ -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,

View file

@ -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),
),
),
),

View file

@ -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<String, double> _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,

View file

@ -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,
),
),
],
);
}
}