From 99df5272ce3a8ed5e56eec914e00f6b963d3adb8 Mon Sep 17 00:00:00 2001 From: Skander Date: Fri, 16 Dec 2022 17:38:30 +0100 Subject: [PATCH 1/2] added zoom, line numbers --- flutter_highlight/lib/flutter_highlight.dart | 212 +++++++++++++++---- 1 file changed, 174 insertions(+), 38 deletions(-) diff --git a/flutter_highlight/lib/flutter_highlight.dart b/flutter_highlight/lib/flutter_highlight.dart index 9afbc47..5ccbb59 100644 --- a/flutter_highlight/lib/flutter_highlight.dart +++ b/flutter_highlight/lib/flutter_highlight.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.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; @@ -27,33 +26,57 @@ class HighlightView extends StatelessWidget { /// Specify text styles such as font family and font size final TextStyle? textStyle; - HighlightView( - String input, { - this.language, - this.theme = const {}, - this.padding, - this.textStyle, - int tabSize = 8, // TODO: https://github.com/flutter/flutter/issues/50087 - }) : source = input.replaceAll('\t', ' ' * tabSize); + /// Enable line numbers + final bool lineNumbers; + + // To enable horizontal scrolling + final bool expanded; + + HighlightView(String input, + {this.language, + this.theme = const {}, + this.padding, + this.textStyle, + int tabSize = 8, // TODO: https://github.com/flutter/flutter/issues/50087 + this.lineNumbers = false, + this.expanded = true}) + : 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 createState() => _HighlightViewState(); +} + +class _HighlightViewState extends State { + double _fontScaleFactor = 1; List _convert(List nodes) { List spans = []; var currentSpans = spans; List> stack = []; - _traverse(Node node) { + traverse(Node node) { 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 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; node.children!.forEach((n) { - _traverse(n); + traverse(n); if (n == node.children!.last) { currentSpans = stack.isEmpty ? spans : stack.removeLast(); } @@ -62,40 +85,153 @@ class HighlightView extends StatelessWidget { } for (var node in nodes) { - _traverse(node); + traverse(node); } return spans; } - 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 Widget build(BuildContext context) { - var _textStyle = TextStyle( - fontFamily: _defaultFontFamily, - color: theme[_rootKey]?.color ?? _defaultFontColor, - ); - if (textStyle != null) { - _textStyle = _textStyle.merge(textStyle); - } + return LayoutBuilder(builder: (context, constraints) { + var tStyle = TextStyle( + fontFamily: HighlightView._defaultFontFamily, + color: widget.theme[HighlightView._rootKey]?.color ?? + HighlightView._defaultFontColor, + ); + if (widget.textStyle != null) { + tStyle = tStyle.merge(widget.textStyle); + } + + var converted = _convert( + highlight.parse(widget.source, language: widget.language).nodes!); + + var painter = TextPainter( + textScaleFactor: _fontScaleFactor, + text: TextSpan(style: tStyle, children: converted), + textAlign: TextAlign.left, + textDirection: TextDirection.ltr, + textWidthBasis: TextWidthBasis.parent, + ); + + painter.layout( + maxWidth: widget.expanded + ? double.infinity + : constraints.maxWidth - tStyle.fontSize! * 3); + if (widget.lineNumbers) { + var lineMetrics = painter.computeLineMetrics(); + assert(lineMetrics.isNotEmpty); + + var realLineNumber = 0; + var lineNumberSpans = List.empty(growable: true); + var maxWidth = 0; + var prevSoftBreak = false; + for (var line in lineMetrics) { + var tmp = (realLineNumber + 1).toString(); + tmp += '\n'; + if (!prevSoftBreak) realLineNumber += 1; + + lineNumberSpans.add(TextSpan(text: (!prevSoftBreak) ? tmp : '\n')); + prevSoftBreak = !line.hardBreak; + + if (tmp.length > maxWidth) maxWidth = tmp.length; + } + + // account for trailing line number + lineNumberSpans.removeLast(); + var tmp = realLineNumber.toString(); + lineNumberSpans.add(TextSpan(text: tmp)); + + return Column( + children: [ + zoomControls(), + Row(children: [ + Container( + decoration: BoxDecoration( + color: widget + .theme[HighlightView._rootKey]?.backgroundColor ?? + HighlightView._defaultBackgroundColor), + child: Padding( + padding: const EdgeInsets.fromLTRB(5, 0, 8, 0), + child: RichText( + textAlign: TextAlign.end, + textScaleFactor: _fontScaleFactor, + text: TextSpan( + style: + widget.textStyle?.copyWith(color: Colors.grey), + children: lineNumberSpans), + ))), + Expanded( + child: Container( + color: + widget.theme[HighlightView._rootKey]?.backgroundColor ?? + HighlightView._defaultBackgroundColor, + child: Scrollbar( + child: SingleChildScrollView( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: CustomPaint( + painter: PainterWrapper(painter), + size: painter.size, + ))))), + ), + ]), + ], + ); + } else { + return CustomPaint( + painter: PainterWrapper(painter), + size: painter.size, + ); + } + }); + } + + Widget zoomControls() { 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, + child: Row( + // mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + color: Colors.grey.shade300, + disabledColor: Colors.grey, + icon: Icon(Icons.zoom_out), + onPressed: _fontScaleFactor > 1 + ? () => setState(() { + _fontScaleFactor -= 0.1; + }) + : null), + IconButton( + icon: Icon(Icons.zoom_in), + color: Colors.grey.shade300, + disabledColor: Colors.grey, + onPressed: _fontScaleFactor < 2 + ? () => setState(() { + _fontScaleFactor += 0.1; + }) + : null), + ], ), ); } } + +class PainterWrapper extends CustomPainter { + final TextPainter textPainter; + + const PainterWrapper(this.textPainter); + + @override + void paint(Canvas canvas, Size size) { + textPainter.paint(canvas, const Offset(4, 0)); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return true; + } +} From dad689895dd0140dd00c0d34459939913cc0cb95 Mon Sep 17 00:00:00 2001 From: Skander Date: Fri, 16 Dec 2022 23:18:40 +0100 Subject: [PATCH 2/2] added custom icons, bug fixes --- flutter_highlight/lib/flutter_highlight.dart | 40 +++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/flutter_highlight/lib/flutter_highlight.dart b/flutter_highlight/lib/flutter_highlight.dart index 5ccbb59..d0c233b 100644 --- a/flutter_highlight/lib/flutter_highlight.dart +++ b/flutter_highlight/lib/flutter_highlight.dart @@ -29,8 +29,11 @@ class HighlightView extends StatefulWidget { /// Enable line numbers final bool lineNumbers; - // To enable horizontal scrolling - final bool expanded; + // To enable control bar + final bool controlBar; + + final Icon zoomInIcon, zoomOutIcon, lineWrapIcon; + final Color? barIconColor; HighlightView(String input, {this.language, @@ -39,7 +42,11 @@ class HighlightView extends StatefulWidget { this.textStyle, int tabSize = 8, // TODO: https://github.com/flutter/flutter/issues/50087 this.lineNumbers = false, - this.expanded = true}) + this.controlBar = false, + this.zoomInIcon = const Icon(Icons.zoom_in), + this.zoomOutIcon = const Icon(Icons.zoom_out), + this.lineWrapIcon = const Icon(Icons.password), + this.barIconColor}) : source = input.replaceAll('\t', ' ' * tabSize); static const _rootKey = 'root'; @@ -57,6 +64,7 @@ class HighlightView extends StatefulWidget { class _HighlightViewState extends State { double _fontScaleFactor = 1; + bool expanded = true; List _convert(List nodes) { List spans = []; @@ -115,7 +123,7 @@ class _HighlightViewState extends State { ); painter.layout( - maxWidth: widget.expanded + maxWidth: expanded ? double.infinity : constraints.maxWidth - tStyle.fontSize! * 3); @@ -145,7 +153,7 @@ class _HighlightViewState extends State { return Column( children: [ - zoomControls(), + if (widget.controlBar) controlBarWidget(), Row(children: [ Container( decoration: BoxDecoration( @@ -188,7 +196,7 @@ class _HighlightViewState extends State { }); } - Widget zoomControls() { + Widget controlBarWidget() { return Container( color: widget.theme[HighlightView._rootKey]?.backgroundColor ?? HighlightView._defaultBackgroundColor, @@ -197,23 +205,35 @@ class _HighlightViewState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ IconButton( - color: Colors.grey.shade300, + color: widget.barIconColor ?? Colors.grey.shade300, disabledColor: Colors.grey, - icon: Icon(Icons.zoom_out), + tooltip: "Zoom out", + icon: widget.zoomOutIcon, onPressed: _fontScaleFactor > 1 ? () => setState(() { _fontScaleFactor -= 0.1; }) : null), IconButton( - icon: Icon(Icons.zoom_in), - color: Colors.grey.shade300, + icon: widget.zoomInIcon, + color: widget.barIconColor ?? Colors.grey.shade300, + tooltip: "Zoom in", disabledColor: Colors.grey, onPressed: _fontScaleFactor < 2 ? () => setState(() { _fontScaleFactor += 0.1; }) : null), + IconButton( + icon: widget.lineWrapIcon, + color: expanded + ? widget.barIconColor ?? Colors.grey.shade300 + : Colors.orange, + tooltip: "Line wrap", + disabledColor: Colors.grey, + onPressed: () => setState(() { + expanded = !expanded; + })), ], ), );