refactor: Use own highlight rendering with working scrollbar and text selection

This commit is contained in:
Christian Kußowski 2025-11-19 09:47:41 +01:00
parent 8e4c61f03b
commit d8d0abf27c
No known key found for this signature in database
GPG key ID: E067ECD60F1A0652
5 changed files with 117 additions and 45 deletions

View file

@ -2,13 +2,13 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_highlighter/flutter_highlighter.dart';
import 'package:flutter_highlighter/themes/shades-of-purple.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:highlight/highlight.dart' show highlight;
import 'package:html/dom.dart' as dom;
import 'package:html/parser.dart' as parser;
import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/code_highlight_theme.dart';
import 'package:fluffychat/utils/event_checkbox_extension.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
@ -137,6 +137,19 @@ class HtmlMessage extends StatelessWidget {
];
}
InlineSpan _renderCodeBlockNode(dom.Node node) {
if (node is! dom.Element) {
return TextSpan(text: node.text);
}
final style = atomOneDarkTheme[node.className.split('-').last] ??
atomOneDarkTheme['root'];
return TextSpan(
children: node.nodes.map(_renderCodeBlockNode).toList(),
style: style,
);
}
/// Transforms a Node to an InlineSpan.
InlineSpan _renderHtml(
dom.Node node,
@ -334,33 +347,60 @@ class HtmlMessage extends StatelessWidget {
);
case 'code':
final isInline = node.parent?.localName != 'pre';
final lang = node.className
.split(' ')
.singleWhereOrNull(
(className) => className.startsWith('language-'),
)
?.split('language-')
.last ??
'md';
final highlightedHtml =
highlight.parse(node.text, language: lang).toHtml();
final element = parser.parse(highlightedHtml).body;
if (element == null) {
return const TextSpan(text: 'Unable to render code block!');
}
final controller = isInline ? null : ScrollController();
return WidgetSpan(
child: Material(
clipBehavior: Clip.hardEdge,
borderRadius: BorderRadius.circular(4),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: HighlightView(
node.text,
language: node.className
.split(' ')
.singleWhereOrNull(
(className) => className.startsWith('language-'),
)
?.split('language-')
.last ??
'md',
theme: shadesOfPurpleTheme,
padding: EdgeInsets.symmetric(
horizontal: 8,
vertical: isInline ? 0 : 8,
),
textStyle: TextStyle(
fontSize: fontSize,
fontFamily: 'RobotoMono',
),
),
color: atomOneBackgroundColor,
shape: RoundedRectangleBorder(
side: const BorderSide(color: hightlightTextColor),
borderRadius: BorderRadius.circular(4),
),
child: isInline
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Text.rich(
TextSpan(
children: [_renderCodeBlockNode(element)],
),
selectionColor: hightlightTextColor.withAlpha(128),
),
)
: RawScrollbar(
thumbVisibility: true,
trackVisibility: true,
controller: controller,
thumbColor: hightlightTextColor,
trackColor: hightlightTextColor.withAlpha(128),
thickness: 8,
child: SingleChildScrollView(
controller: controller,
scrollDirection: Axis.horizontal,
child: Padding(
padding: const EdgeInsets.all(4.0),
child: Text.rich(
TextSpan(
children: [_renderCodeBlockNode(element)],
),
selectionColor: hightlightTextColor.withAlpha(212),
),
),
),
),
),
);
case 'img':

View file

@ -0,0 +1,40 @@
import 'package:flutter/widgets.dart';
const hightlightTextColor = Color(0xffabb2bf);
const atomOneBackgroundColor = Color(0xff282c34);
const atomOneDarkTheme = {
'root': TextStyle(color: hightlightTextColor),
'comment': TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic),
'quote': TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic),
'doctag': TextStyle(color: Color(0xffc678dd)),
'keyword': TextStyle(color: Color(0xffc678dd)),
'formula': TextStyle(color: Color(0xffc678dd)),
'section': TextStyle(color: Color(0xffe06c75)),
'name': TextStyle(color: Color(0xffe06c75)),
'selector-tag': TextStyle(color: Color(0xffe06c75)),
'deletion': TextStyle(color: Color(0xffe06c75)),
'subst': TextStyle(color: Color(0xffe06c75)),
'literal': TextStyle(color: Color(0xff56b6c2)),
'string': TextStyle(color: Color(0xff98c379)),
'regexp': TextStyle(color: Color(0xff98c379)),
'addition': TextStyle(color: Color(0xff98c379)),
'attribute': TextStyle(color: Color(0xff98c379)),
'meta-string': TextStyle(color: Color(0xff98c379)),
'built_in': TextStyle(color: Color(0xffe6c07b)),
'attr': TextStyle(color: Color(0xffd19a66)),
'variable': TextStyle(color: Color(0xffd19a66)),
'template-variable': TextStyle(color: Color(0xffd19a66)),
'type': TextStyle(color: Color(0xffd19a66)),
'selector-class': TextStyle(color: Color(0xffd19a66)),
'selector-attr': TextStyle(color: Color(0xffd19a66)),
'selector-pseudo': TextStyle(color: Color(0xffd19a66)),
'number': TextStyle(color: Color(0xffd19a66)),
'symbol': TextStyle(color: Color(0xff61aeee)),
'bullet': TextStyle(color: Color(0xff61aeee)),
'link': TextStyle(color: Color(0xff61aeee)),
'meta': TextStyle(color: Color(0xff61aeee)),
'selector-id': TextStyle(color: Color(0xff61aeee)),
'title': TextStyle(color: Color(0xff61aeee)),
'emphasis': TextStyle(fontStyle: FontStyle.italic),
'strong': TextStyle(fontWeight: FontWeight.bold),
};

View file

@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_highlighter/flutter_highlighter.dart';
import 'package:flutter_highlighter/themes/shades-of-purple.dart';
import 'package:matrix/matrix.dart';
import 'package:url_launcher/url_launcher.dart';
@ -40,10 +38,12 @@ class ErrorReporter {
height: 256,
width: 256,
child: SingleChildScrollView(
child: HighlightView(
child: Text(
text,
language: 'sh',
theme: shadesOfPurpleTheme,
style: const TextStyle(
fontSize: 14,
fontFamily: 'RobotoMono',
),
),
),
),

View file

@ -483,14 +483,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "9.1.0"
flutter_highlighter:
dependency: "direct main"
description:
name: flutter_highlighter
sha256: "93173afd47a9ada53f3176371755e7ea4a1065362763976d06d6adfb4d946e10"
url: "https://pub.dev"
source: hosted
version: "0.1.1"
flutter_linkify:
dependency: "direct main"
description:
@ -799,14 +791,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.4.0"
highlighter:
dependency: transitive
highlight:
dependency: "direct main"
description:
name: highlighter
sha256: "92180c72b9da8758e1acf39a45aa305a97dcfe2fdc8f3d1d2947c23f2772bfbc"
name: highlight
sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21"
url: "https://pub.dev"
source: hosted
version: "0.1.1"
version: "0.7.0"
html:
dependency: "direct main"
description:

View file

@ -30,7 +30,6 @@ dependencies:
flutter:
sdk: flutter
flutter_foreground_task: ^9.1.0
flutter_highlighter: ^0.1.1
flutter_linkify: ^6.0.0
flutter_local_notifications: ^19.5.0
flutter_localizations:
@ -45,6 +44,7 @@ dependencies:
geolocator: ^14.0.2
go_router: ^17.0.0
handy_window: ^0.4.0
highlight: ^0.7.0
html: ^0.15.4
http: ^1.6.0
image: ^4.1.7