304 lines
12 KiB
Dart
304 lines
12 KiB
Dart
import 'dart:io';
|
|
import 'dart:math';
|
|
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:subwrite/theme.dart';
|
|
|
|
import 'annotation.dart';
|
|
import 'cursor.dart';
|
|
import 'file.dart';
|
|
import 'highlighter.dart';
|
|
|
|
const double fontSize = 14;
|
|
const double gutterFontSize = 12;
|
|
const double gutterWidth = 64;
|
|
const double rightMargin = 32;
|
|
const double leftMargin = 5.0;
|
|
const charWidth = (fontSize / 2.0);
|
|
const lineHeight = (fontSize * 1.2);
|
|
|
|
class LineInfo extends ChangeNotifier {
|
|
String path;
|
|
String text;
|
|
int lineNumber;
|
|
int? cursorStart;
|
|
int? cursorEnd;
|
|
bool lineSelected = false;
|
|
|
|
List<LineAnnotation> annotations = List.empty(growable: true);
|
|
|
|
LineInfo(this.path, this.lineNumber, this.text);
|
|
|
|
Widget build(BuildContext context, AnimationController animationController) {
|
|
bool highlightRisk = false;
|
|
|
|
var backgroundStyle = TextStyle(backgroundColor: cursorStart != null ? sidebarHighlight : background);
|
|
|
|
final gutterStyle = TextStyle(fontFamily: 'Iosevka', decoration: null, decorationColor: null, fontSize: gutterFontSize, color: comment, backgroundColor: sidebarAlt, height: 1.0);
|
|
|
|
var sidebarContent = List<Widget>.empty(growable: true);
|
|
sidebarContent.add(Text('$lineNumber'.padLeft(5), style: gutterStyle));
|
|
|
|
var icons = annotations.where((element) => element.annotationType == AnnotationType.note).map((e) {
|
|
highlightRisk = true;
|
|
return Padding(padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 0.0), child: Tooltip(message: e.description!, child: e.getIcon(15.0)));
|
|
}).toList();
|
|
|
|
sidebarContent.addAll(icons);
|
|
|
|
icons = annotations.where((element) => element.annotationType == AnnotationType.todo).map((e) {
|
|
highlightRisk = true;
|
|
return Padding(padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 0.0), child: e.getIcon(15.0));
|
|
}).toList();
|
|
|
|
sidebarContent.addAll(icons);
|
|
|
|
LineAnnotation? image;
|
|
if (annotations.any((element) => element.annotationType == AnnotationType.image)) {
|
|
image = annotations.firstWhere((element) => element.annotationType == AnnotationType.image);
|
|
}
|
|
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
var usableSize = (constraints.maxWidth - gutterWidth - rightMargin - leftMargin).floorToDouble();
|
|
var charWidth = (fontSize / 2.0);
|
|
var lineHeight = (fontSize * 1.2);
|
|
var maxCharsBeforeWrapping = (usableSize / charWidth).floor();
|
|
|
|
Highlighter hl = Highlighter();
|
|
List<InlineSpan> spans = hl.run(context, text, lineNumber, highlightRisk, backgroundStyle, annotations, cursorStart, cursorEnd, lineSelected, maxCharsBeforeWrapping);
|
|
|
|
var highlightedLine = RichText(
|
|
softWrap: true,
|
|
textWidthBasis: TextWidthBasis.longestLine,
|
|
strutStyle: StrutStyle.disabled,
|
|
textAlign: TextAlign.left,
|
|
text: TextSpan(
|
|
children: spans,
|
|
style: backgroundStyle,
|
|
),
|
|
);
|
|
|
|
List<Widget> stackElements = List.empty(growable: true);
|
|
|
|
var idealHeight = (lineHeight * (1.0 + ((text.length * charWidth) / usableSize).floorToDouble())).ceilToDouble();
|
|
|
|
if (cursorStart != null || image == null) {
|
|
var highlightedLineContainer = Container(
|
|
padding: EdgeInsets.zero,
|
|
margin: EdgeInsets.only(right: rightMargin, top: 0, bottom: 0, left: leftMargin),
|
|
decoration: BoxDecoration(
|
|
color: backgroundStyle.backgroundColor,
|
|
),
|
|
width: usableSize,
|
|
height: idealHeight,
|
|
child: highlightedLine,
|
|
);
|
|
|
|
stackElements.add(highlightedLineContainer);
|
|
} else {
|
|
var file = File(image.type);
|
|
idealHeight = 250;
|
|
var imageContainer = Container(
|
|
padding: EdgeInsets.zero,
|
|
margin: EdgeInsets.only(right: rightMargin, top: 0, bottom: 0, left: leftMargin),
|
|
decoration: BoxDecoration(
|
|
color: backgroundStyle.backgroundColor,
|
|
),
|
|
width: usableSize,
|
|
child: Image.file(
|
|
file,
|
|
height: idealHeight,
|
|
width: usableSize / 2.0,
|
|
fit: BoxFit.contain,
|
|
filterQuality: FilterQuality.high,
|
|
cacheHeight: idealHeight.floor(),
|
|
));
|
|
stackElements.add(imageContainer);
|
|
}
|
|
|
|
if (cursorStart != null) {
|
|
var maxCharsBeforeWrapping = (usableSize / charWidth).floorToDouble();
|
|
var cursorPosTop = ((cursorStart! / maxCharsBeforeWrapping).floorToDouble() * lineHeight).roundToDouble();
|
|
var cursorPosLeft = max(0, (((cursorStart! % maxCharsBeforeWrapping) * charWidth) - charWidth / 2.0).roundToDouble());
|
|
TextStyle cursorStyle = TextStyle(fontFamily: 'Iosevka', fontSize: fontSize, color: foreground, backgroundColor: Colors.transparent, letterSpacing: -2.0);
|
|
var cursorPos =
|
|
Positioned(top: cursorPosTop.toDouble(), left: leftMargin + cursorPosLeft.toDouble(), child: FadeTransition(opacity: animationController, child: Text("|", style: cursorStyle)));
|
|
stackElements.add(cursorPos);
|
|
}
|
|
|
|
var cursorOverlay = Stack(children: stackElements);
|
|
|
|
return Listener(
|
|
onPointerDown: (event) {
|
|
var inMargin = (event.localPosition.dx - gutterWidth) > usableSize;
|
|
var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy);
|
|
var file = Provider.of<LineFile>(context, listen: false);
|
|
if (event.down && !inMargin) {
|
|
file.mouseDownUpdate(lineNumber, c.round());
|
|
}
|
|
|
|
var set = false;
|
|
|
|
if (event.buttons == kSecondaryMouseButton) {
|
|
for (var annot in annotations) {
|
|
if (annot.tool == "spellcheck") {
|
|
if (annot.sourceLink.colStart != null && annot.sourceLink.colEnd != null) {
|
|
if (annot.sourceLink.colStart! <= c && annot.sourceLink.colEnd! > c) {
|
|
set = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (set) {
|
|
Provider.of<LineFile>(context).showSuggestionsFor(lineNumber, c);
|
|
}
|
|
}
|
|
},
|
|
onPointerMove: (event) {
|
|
var inMargin = (event.localPosition.dx - gutterWidth) > usableSize;
|
|
var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy);
|
|
var file = Provider.of<LineFile>(context, listen: false);
|
|
if (event.down && !inMargin) {
|
|
file.mouseDownMoveUpdate(c.round());
|
|
} else {
|
|
file.mouseDownEnd();
|
|
}
|
|
},
|
|
child: MouseRegion(
|
|
onEnter: (event) {
|
|
var inMargin = (event.localPosition.dx - gutterWidth) > usableSize;
|
|
var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy);
|
|
var file = Provider.of<LineFile>(context, listen: false);
|
|
if (event.down && !inMargin) {
|
|
file.mouseDownUpdate(lineNumber, c.round());
|
|
} else {
|
|
file.mouseDownEnd();
|
|
}
|
|
},
|
|
onHover: (event) {
|
|
var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy);
|
|
var file = Provider.of<LineFile>(context, listen: false);
|
|
|
|
if (event.down) {
|
|
// note should never happen because flutter only fires hover events when the mouse it up...
|
|
file.mouseDownUpdate(lineNumber, c.round());
|
|
} else {
|
|
file.mouseDownEnd();
|
|
}
|
|
},
|
|
onExit: (event) {},
|
|
child: Container(
|
|
color: backgroundStyle.backgroundColor,
|
|
padding: EdgeInsets.zero,
|
|
margin: EdgeInsets.zero,
|
|
height: idealHeight,
|
|
width: (usableSize + gutterWidth + rightMargin + leftMargin).floorToDouble(),
|
|
child: Row(mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
Container(
|
|
decoration: BoxDecoration(color: sidebarAlt),
|
|
// row can only have gutterWidth+margin wide
|
|
width: (gutterWidth - 2).floorToDouble(),
|
|
height: idealHeight,
|
|
margin: const EdgeInsets.only(right: 1, top: 0, bottom: 0, left: 0),
|
|
padding: EdgeInsets.zero,
|
|
alignment: Alignment.centerLeft,
|
|
child: Row(mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: sidebarContent)),
|
|
cursorOverlay,
|
|
]))));
|
|
},
|
|
);
|
|
}
|
|
|
|
void setCursor(Cursor cursor) {
|
|
if (cursor.hasSelection()) {
|
|
// if we are part of a selection then act like it
|
|
if (cursor.line < lineNumber && cursor.anchorLine > lineNumber) {
|
|
if (lineSelected == false) {
|
|
lineSelected = true;
|
|
cursorStart = null;
|
|
cursorEnd = null;
|
|
notifyListeners();
|
|
}
|
|
} else if (cursor.line == lineNumber && cursor.anchorLine > lineNumber) {
|
|
// else if the selection starts on this line...
|
|
// obliterate selection
|
|
if (lineSelected == true) {
|
|
lineSelected = false;
|
|
}
|
|
cursorStart = cursor.column;
|
|
cursorEnd = null;
|
|
notifyListeners();
|
|
} else if (cursor.line < lineNumber && cursor.anchorLine == lineNumber) {
|
|
// else if the selection ends on this line...
|
|
// obliterate selection
|
|
if (lineSelected == true) {
|
|
lineSelected = false;
|
|
}
|
|
cursorStart = null;
|
|
cursorEnd = cursor.anchorColumn;
|
|
notifyListeners();
|
|
} else if (cursor.line == cursor.line && cursor.anchorLine == lineNumber) {
|
|
if (lineSelected == true) {
|
|
lineSelected = false;
|
|
}
|
|
cursorStart = cursor.column;
|
|
cursorEnd = cursor.anchorColumn;
|
|
notifyListeners();
|
|
} else {
|
|
// obliterate selection
|
|
if (lineSelected == true) {
|
|
lineSelected = false;
|
|
notifyListeners();
|
|
}
|
|
if (cursorStart != null) {
|
|
cursorStart = null;
|
|
notifyListeners();
|
|
}
|
|
if (cursorEnd != null) {
|
|
cursorEnd = null;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
} else {
|
|
// obliterate selection
|
|
if (lineSelected == true) {
|
|
lineSelected = false;
|
|
cursorStart = null;
|
|
cursorEnd = null;
|
|
notifyListeners();
|
|
}
|
|
if (cursor.line == lineNumber) {
|
|
cursorStart = cursor.column;
|
|
cursorEnd = cursor.anchorColumn;
|
|
notifyListeners();
|
|
} else if (cursorStart != null || cursorEnd != null) {
|
|
cursorStart = null;
|
|
cursorEnd = null;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
}
|
|
|
|
void updateText(String line) {
|
|
text = line;
|
|
notifyListeners();
|
|
}
|
|
|
|
void notify() {
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
int cursorFromMouse(double usableSize, double mx, double my) {
|
|
var charWidth = (fontSize / 2.0);
|
|
var lineHeight = (fontSize * 1.2);
|
|
var maxCharsBeforeWrapping = (usableSize / charWidth).floorToDouble();
|
|
var prevLines = (my / lineHeight).floorToDouble() * maxCharsBeforeWrapping;
|
|
var charsInThisLine = ((mx) / charWidth).floorToDouble();
|
|
return (prevLines + charsInThisLine).floor();
|
|
}
|