Skip to content

Background rendering for Flutter #35

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
8 changes: 8 additions & 0 deletions flutter_highlight/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ class MyWidget extends StatelessWidget {
}
```

### Background processing

Processing large amounts of text can be slow. To perform text processing in the background, add a
`HighlightBackgroundEnvironment` above any `HighlightView`s in your widget tree.

A background isolate will be automatically started in `HighlightBackgroundEnvironment.initState`, and stopped in
`HighlightBackgroundEnvironment.dispose`.

## References

- [All available languages](https://github.com/pd4d10/highlight/tree/master/highlight/lib/languages)
Expand Down
20 changes: 12 additions & 8 deletions flutter_highlight/example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_highlight/flutter_highlight.dart';
import 'package:flutter_highlight/flutter_highlight_background.dart';
import 'package:flutter_highlight/theme_map.dart';
import 'package:url_launcher/url_launcher.dart';

import 'example_map.dart';

void main() => runApp(MyApp());
Expand Down Expand Up @@ -95,14 +97,16 @@ class _MyHomePageState extends State<MyHomePage> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
HighlightView(
exampleMap[language],
language: language,
theme: themeMap[theme],
padding: EdgeInsets.all(12),
textStyle: TextStyle(
fontFamily:
'SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace'),
HighlightBackgroundEnvironment(
child: HighlightView(
exampleMap[language],
language: language,
theme: themeMap[theme],
padding: EdgeInsets.all(12),
textStyle: TextStyle(
fontFamily:
'SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace'),
),
)
],
),
Expand Down
133 changes: 111 additions & 22 deletions flutter_highlight/lib/flutter_highlight.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/widgets.dart';
import 'package:flutter_highlight/flutter_highlight_background.dart';
import 'package:highlight/highlight.dart' show highlight, Node;

/// Highlight Flutter Widget
class HighlightView extends StatelessWidget {
class HighlightView extends StatefulWidget {
/// The original code to be highlighted
final String source;

Expand All @@ -27,16 +29,40 @@ class HighlightView extends StatelessWidget {
/// Specify text styles such as font family and font size
final TextStyle? textStyle;

/// Progress indicator
///
/// A widget that is displayed while the [source] is being processed.
/// This may only be used if a [HighlightBackgroundEnvironment] is available.
final Widget? progressIndicator;

HighlightView(
String input, {
this.language,
this.theme = const {},
this.padding,
this.textStyle,
int tabSize = 8, // TODO: https://github.com/flutter/flutter/issues/50087
this.progressIndicator,
}) : source = input.replaceAll('\t', ' ' * tabSize);

List<TextSpan> _convert(List<Node> nodes) {
static const _rootKey = 'root';
static const _defaultFontColor = Color(0xff000000);
static const _defaultBackgroundColor = Color(0xffffffff);

// TODO: dart:io is not available at web platform currently
// See: https://github.com/flutter/flutter/issues/39998
// So we just use monospace here for now
static const _defaultFontFamily = 'monospace';

@override
State<HighlightView> createState() => _HighlightViewState();

/// Renders a list of [nodes] into a list of [TextSpan]s using the given
/// [theme].
static List<TextSpan> render(
List<Node> nodes,
Map<String, TextStyle> theme,
) {
List<TextSpan> spans = [];
var currentSpans = spans;
List<List<TextSpan>> stack = [];
Expand All @@ -48,7 +74,8 @@ class HighlightView extends StatelessWidget {
: TextSpan(text: node.value, style: theme[node.className!]));
} else if (node.children != null) {
List<TextSpan> tmp = [];
currentSpans.add(TextSpan(children: tmp, style: theme[node.className!]));
currentSpans
.add(TextSpan(children: tmp, style: theme[node.className!]));
stack.add(currentSpans);
currentSpans = tmp;

Expand All @@ -67,34 +94,96 @@ class HighlightView extends StatelessWidget {

return spans;
}
}

static const _rootKey = 'root';
static const _defaultFontColor = Color(0xff000000);
static const _defaultBackgroundColor = Color(0xffffffff);
class _HighlightViewState extends State<HighlightView> {
late Future<List<Node>> _nodesFuture;
late Future<List<TextSpan>> _spansFuture;

// TODO: dart:io is not available at web platform currently
// See: https://github.com/flutter/flutter/issues/39998
// So we just use monospace here for now
static const _defaultFontFamily = 'monospace';
void _parse(HighlightBackgroundProvider? backgroundProvider) => _nodesFuture =
backgroundProvider?.parse(widget.source, language: widget.language) ??
Future.value(
highlight.parse(widget.source, language: widget.language).nodes,
);

void _render(HighlightBackgroundProvider? backgroundProvider) =>
_spansFuture = _nodesFuture.then((nodes) =>
(backgroundProvider?.render(nodes, widget.theme) ??
HighlightView.render(nodes, widget.theme))
as FutureOr<List<TextSpan>>);

void _parseAndRender(HighlightBackgroundProvider? backgroundProvider) {
if (backgroundProvider == null) {
_parse(null);
_render(null);
} else {
final resultFuture = backgroundProvider.parseAndRender(
widget.source,
widget.theme,
language: widget.language,
);
_nodesFuture = resultFuture.then((result) => result.nodes);
_spansFuture = resultFuture.then((result) => result.spans);
}
}

@override
void didChangeDependencies() {
super.didChangeDependencies();
final backgroundProvider = HighlightBackgroundProvider.maybeOf(context);
_parseAndRender(backgroundProvider);
}

@override
void didUpdateWidget(HighlightView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.source != oldWidget.source ||
widget.language != oldWidget.language) {
final backgroundProvider = HighlightBackgroundProvider.maybeOf(context);
_parseAndRender(backgroundProvider);
} else if (widget.theme != oldWidget.theme) {
final backgroundProvider = HighlightBackgroundProvider.maybeOf(context);
_render(backgroundProvider);
}
}

@override
Widget build(BuildContext context) {
var _textStyle = TextStyle(
fontFamily: _defaultFontFamily,
color: theme[_rootKey]?.color ?? _defaultFontColor,
fontFamily: HighlightView._defaultFontFamily,
color: widget.theme[HighlightView._rootKey]?.color ??
HighlightView._defaultFontColor,
);
if (textStyle != null) {
_textStyle = _textStyle.merge(textStyle);
if (widget.textStyle != null) {
_textStyle = _textStyle.merge(widget.textStyle);
}

return Container(
color: theme[_rootKey]?.backgroundColor ?? _defaultBackgroundColor,
padding: padding,
child: RichText(
text: TextSpan(
style: _textStyle,
children: _convert(highlight.parse(source, language: language).nodes!),
),
color: widget.theme[HighlightView._rootKey]?.backgroundColor ??
HighlightView._defaultBackgroundColor,
padding: widget.padding,
child: FutureBuilder<List<TextSpan>>(
future: _spansFuture,
builder: (context, snapshot) {
if (!snapshot.hasData) {
final progressIndicator = widget.progressIndicator;
if (progressIndicator == null) {
return const SizedBox.shrink();
} else {
assert(
HighlightBackgroundProvider.maybeOf(context) != null,
'Cannot display a progress indicator unless a HighlightBackgroundEnvironment is available!',
);
return progressIndicator;
}
}
return RichText(
text: TextSpan(
style: _textStyle,
children: snapshot.requireData,
),
);
},
),
);
}
Expand Down
Loading