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 processing for Flutter #34

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
108 changes: 86 additions & 22 deletions flutter_highlight/lib/flutter_highlight.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
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,15 +27,39 @@ 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);

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();
}

class _HighlightViewState extends State<HighlightView> {
late Future<List<Node>> _nodesFuture;
late Future<List<TextSpan>> _spansFuture;

List<TextSpan> _convert(List<Node> nodes) {
List<TextSpan> spans = [];
var currentSpans = spans;
Expand All @@ -45,10 +69,11 @@ class HighlightView extends StatelessWidget {
if (node.value != null) {
currentSpans.add(node.className == null
? TextSpan(text: node.value)
: TextSpan(text: node.value, style: theme[node.className!]));
: TextSpan(text: node.value, style: widget.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: widget.theme[node.className!]));
stack.add(currentSpans);
currentSpans = tmp;

Expand All @@ -68,33 +93,72 @@ class HighlightView extends StatelessWidget {
return spans;
}

static const _rootKey = 'root';
static const _defaultFontColor = Color(0xff000000);
static const _defaultBackgroundColor = Color(0xffffffff);
void _parse(HighlightBackgroundProvider? backgroundProvider) => _nodesFuture =
backgroundProvider?.parse(widget.source, language: widget.language) ??
Future.value(
highlight.parse(widget.source, language: widget.language).nodes,
);

// 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 _generateSpans() => _spansFuture = _nodesFuture.then(_convert);

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

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

@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
151 changes: 151 additions & 0 deletions flutter_highlight/lib/flutter_highlight_background.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import 'dart:async';
import 'dart:isolate';

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

/// A widget that provides a background [Isolate] to do expensive highlighting
/// work in.
///
/// The [HighlightView] will detect and use the background environment
/// automatically.
/// It can also be used manually through the [HighlightBackgroundProvider]
/// [InheritedWidget].
class HighlightBackgroundEnvironment extends StatefulWidget {
final Widget child;

const HighlightBackgroundEnvironment({
Key? key,
required this.child,
}) : super(key: key);

@override
State<HighlightBackgroundEnvironment> createState() =>
_HighlightBackgroundEnvironmentState();
}

class _HighlightBackgroundEnvironmentState
extends State<HighlightBackgroundEnvironment> {
late final Completer<SendPort> _sendPortCompleter;
late final StreamController<_ParseResponse> _parseResultStreamController;

@override
void initState() {
super.initState();
_sendPortCompleter = Completer();
_parseResultStreamController = StreamController.broadcast();
final receivePort = ReceivePort();
receivePort.listen((message) {
if (message is _ParseResponse) {
_parseResultStreamController.add(message);
} else if (message is _IsolateStartedResponse) {
_sendPortCompleter.complete(message.sendPort);
} else if (message is _IsolateEndedResponse) {
receivePort.close();
}
});
Isolate.spawn(_isolateEntrypoint, receivePort.sendPort);
}

@override
void dispose() {
super.dispose();
_sendPortCompleter.future
.then((sendPort) => sendPort.send(_IsolateEndRequest()));
}

Future<List<Node>> parse(String source, {String? language}) {
final identifier = Capability();
_sendPortCompleter.future.then((sendPort) =>
sendPort.send(_ParseRequest(identifier, source, language: language)));
return _parseResultStreamController.stream
.firstWhere((message) => identical(message.identifier, identifier))
.then((message) => message.nodes);
}

@override
Widget build(BuildContext context) {
return HighlightBackgroundProvider._(
environmentIdentifier: this,
parse: parse,
child: widget.child,
);
}
}

class HighlightBackgroundProvider extends InheritedWidget {
final Object environmentIdentifier;
final Future<List<Node>> Function(String source, {String? language}) parse;

HighlightBackgroundProvider._({
Key? key,
required this.environmentIdentifier,
required this.parse,
required Widget child,
}) : super(
key: key,
child: child,
);

@override
bool updateShouldNotify(HighlightBackgroundProvider oldWidget) =>
!identical(environmentIdentifier, oldWidget.environmentIdentifier);

static HighlightBackgroundProvider of(BuildContext context) {
final backgroundProvider = maybeOf(context);
assert(backgroundProvider != null,
'No HighlightBackgroundProvider found in context');
return backgroundProvider!;
}

static HighlightBackgroundProvider? maybeOf(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<HighlightBackgroundProvider>();
}

void _isolateEntrypoint(SendPort sendPort) {
final receivePort = ReceivePort();
receivePort.listen((message) {
if (message is _ParseRequest) {
final nodes =
highlight.parse(message.source, language: message.language).nodes!;
sendPort.send(_ParseResponse(message.identifier, nodes));
} else if (message is _IsolateEndRequest) {
receivePort.close();
sendPort.send(const _IsolateEndedResponse());
}
});
sendPort.send(_IsolateStartedResponse(receivePort.sendPort));
}

abstract class _IsolateRequest {}

class _IsolateEndRequest implements _IsolateRequest {
const _IsolateEndRequest();
}

class _ParseRequest implements _IsolateRequest {
final Capability identifier;
final String source;
final String? language;

const _ParseRequest(this.identifier, this.source, {this.language});
}

abstract class _IsolateResponse {}

class _IsolateStartedResponse implements _IsolateResponse {
final SendPort sendPort;

const _IsolateStartedResponse(this.sendPort);
}

class _IsolateEndedResponse implements _IsolateResponse {
const _IsolateEndedResponse();
}

class _ParseResponse implements _IsolateResponse {
final Capability identifier;
final List<Node> nodes;

const _ParseResponse(this.identifier, this.nodes);
}