subwrite/lib/line_info.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();
}