Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Background rendering for Flutter #35

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