From cdbe4d8c9b9b441ed7c5ed6500d1776b7f0a26fc Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Sat, 8 Oct 2022 18:24:24 -0700 Subject: [PATCH] Line Jump / Focus / Cursor Fixes / Home/End Selection bug / Cursor Blink --- lib/file.dart | 24 +++++++------------ lib/input.dart | 14 +++-------- lib/line_info.dart | 58 +++++++++++++++++++++++++--------------------- lib/main.dart | 10 ++++++-- lib/outline.dart | 2 +- lib/view.dart | 41 ++++++++++++++++++-------------- pubspec.lock | 9 ++++++- pubspec.yaml | 1 + 8 files changed, 83 insertions(+), 76 deletions(-) diff --git a/lib/file.dart b/lib/file.dart index 2670a28..4127e94 100644 --- a/lib/file.dart +++ b/lib/file.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:subwrite/annotation.dart'; import 'package:subwrite/section.dart'; @@ -19,7 +20,7 @@ class LineFile extends ChangeNotifier { late String projectPath; late String path; - ScrollController scrollController = ScrollController(); + ItemScrollController scrollController = ItemScrollController(); void openFile(String path, int startLine) { this.path = path; @@ -37,7 +38,6 @@ class LineFile extends ChangeNotifier { parse(); cursor.moveCursorToDocStart(); focus.requestFocus(); - scrollController = ScrollController(initialScrollOffset: (startLine * 17.0 - (17.0 * 10.0)).clamp(0.0, double.infinity)); notifyListeners(); } @@ -57,12 +57,12 @@ class LineFile extends ChangeNotifier { cursor.moveCursor(0, 1, backingLines, keepAnchor: event.isShiftPressed); cursor.publishCursor(backingLines); } else if (event.logicalKey == LogicalKeyboardKey.home) { - cursor.clearSelection(); cursor.moveCursorToLineStart(); + cursor.clearSelection(); cursor.publishCursor(backingLines); } else if (event.logicalKey == LogicalKeyboardKey.end) { - cursor.clearSelection(); cursor.moveCursorToLineEnd(backingLines[cursor.line - 1].text.length); + cursor.clearSelection(); cursor.publishCursor(backingLines); } else if (event.logicalKey == LogicalKeyboardKey.enter) { insertLine(); @@ -116,7 +116,7 @@ class LineFile extends ChangeNotifier { void saveFile() { String content = ''; for (var l in backingLines) { - content += l.text + '\n'; + content += '${l.text}\n'; } File(path).writeAsStringSync(content); } @@ -161,7 +161,6 @@ class LineFile extends ChangeNotifier { // Delete 2nd to n-1 Lines for (var line = (ncursor.anchorLine - 1); line >= ncursor.line; line -= 1) { - print("deleting $line ${backingLines[line].text}"); backingLines.removeAt(line); } renumberFrom(ncursor.line); @@ -306,19 +305,11 @@ class LineFile extends ChangeNotifier { notifyListeners(); } - String base64() { - String content = ''; - backingLines.forEach((l) { - content += l.text + '\n'; - }); - return base64Encode(utf8.encode(content)); - } - void copy() { var ncursor = cursor.normalized(); - var firstLine = backingLines[ncursor.line - 1].text.substring(ncursor.column) + "\n"; + var firstLine = "${backingLines[ncursor.line - 1].text.substring(ncursor.column)}\n"; for (var line = ncursor.line + 1; line < ncursor.anchorLine; line++) { - firstLine += backingLines[line - 1].text + "\n"; + firstLine += "${backingLines[line - 1].text}\n"; } var textToCopy = firstLine; if (ncursor.anchorLine != ncursor.line) { @@ -345,6 +336,7 @@ class LineFile extends ChangeNotifier { } void mouseDownUpdate(int line, int col) { + focus.requestFocus(); cursor.mouseDownUpdate(line, col, backingLines); } diff --git a/lib/input.dart b/lib/input.dart index b8bea14..4c877d4 100644 --- a/lib/input.dart +++ b/lib/input.dart @@ -13,18 +13,14 @@ class InputListener extends StatefulWidget { } class _InputListener extends State { - late FocusNode focusNode; - @override void initState() { super.initState(); - focusNode = FocusNode(); } @override void dispose() { super.dispose(); - focusNode.dispose(); } void newTextDialog(context, String title, String hint, Function(String) callback) { @@ -98,16 +94,11 @@ class _InputListener extends State { @override Widget build(BuildContext context) { - if (!focusNode.hasFocus) { - focusNode.requestFocus(); - } - LineFile doc = Provider.of(context); return GestureDetector( child: Focus( - child: widget.child, - focusNode: focusNode, + focusNode: doc.focus, autofocus: true, onKey: (FocusNode node, RawKeyEvent event) { doc.handleKey(event); @@ -115,6 +106,7 @@ class _InputListener extends State { newTextDialog(context, "New Note", "Note", (note) => {doc.addNote(note)}); } return KeyEventResult.handled; - })); + }, + child: widget.child)); } } diff --git a/lib/line_info.dart b/lib/line_info.dart index 1af0117..1890ac8 100644 --- a/lib/line_info.dart +++ b/lib/line_info.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:subwrite/theme.dart'; @@ -9,6 +11,7 @@ import 'highlighter.dart'; double gutterWidth = 64; double rightMargin = 32; +double leftMargin = 5.0; class LineInfo extends ChangeNotifier { String path; @@ -17,11 +20,12 @@ class LineInfo extends ChangeNotifier { int? cursorStart; int? cursorEnd; bool lineSelected = false; + List annotations = List.empty(growable: true); LineInfo(this.path, this.lineNumber, this.text); - Widget build(BuildContext context) { + Widget build(BuildContext context, AnimationController animationController) { bool highlightRisk = false; var backgroundStyle = TextStyle(backgroundColor: cursorStart != null ? sidebarHighlight : background); @@ -41,11 +45,11 @@ class LineInfo extends ChangeNotifier { ); List stackElements = List.empty(growable: true); - stackElements.add(highlightedLine); + var charWidth = (fontSize / 2.0); var lineHeight = (fontSize * 1.2); - var usableSize = (constraints.maxWidth - gutterWidth - rightMargin); + var usableSize = (constraints.maxWidth - gutterWidth - rightMargin - leftMargin); var sidebarContent = List.empty(growable: true); sidebarContent.add(Text('$lineNumber'.padLeft(5), style: gutterStyle)); @@ -55,35 +59,47 @@ class LineInfo extends ChangeNotifier { return Padding(padding: EdgeInsets.symmetric(horizontal: 5.0, vertical: 0.0), child: Tooltip(message: e.description!, child: e.getIcon(15.0))); }).toList(); - if (icons != null) { - sidebarContent.addAll(icons); - } + sidebarContent.addAll(icons); icons = annotations.where((element) => element.annotationType == AnnotationType.todo).map((e) { highlightRisk = true; return Padding(padding: EdgeInsets.symmetric(horizontal: 5.0, vertical: 0.0), child: e.getIcon(15.0)); }).toList(); - if (icons != null) { - sidebarContent.addAll(icons); - } + sidebarContent.addAll(icons); + + var idealHeight = (lineHeight * (1.0 + ((text.length * charWidth) / usableSize).floor())).ceilToDouble(); + + var highlightedLineContainer = Container( + padding: EdgeInsets.only(right: 0, top: 0, bottom: 0, left: 0), + margin: EdgeInsets.only(right: rightMargin, top: 0, bottom: 0, left: leftMargin), + decoration: BoxDecoration( + color: backgroundStyle.backgroundColor, + ), + width: constraints.maxWidth - rightMargin - gutterWidth - leftMargin, + height: idealHeight, + child: highlightedLine, + ); + + stackElements.add(highlightedLineContainer); if (cursorStart != null) { var maxCharsBeforeWrapping = (usableSize / charWidth).floorToDouble(); var cursorPosTop = (cursorStart! / maxCharsBeforeWrapping).floorToDouble() * lineHeight; var cursorPosLeft = ((cursorStart! % maxCharsBeforeWrapping) * charWidth) - charWidth / 2.0; - TextStyle cusrsorStyle = TextStyle(fontFamily: 'Iosevka', fontSize: fontSize, color: foreground, backgroundColor: Colors.transparent, letterSpacing: -2.0); - var cursorPos = Positioned(top: cursorPosTop.toDouble(), left: cursorPosLeft.toDouble(), child: Text("|", style: cusrsorStyle)); - stackElements.add(cursorPos); + 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 idealHeight = (lineHeight * (1.0 + ((text.length * charWidth) / usableSize).floor())).ceilToDouble(); var cursorOverlay = Stack(children: stackElements); + + return Listener( onPointerDown: (event) { - //var c = (event.localPosition.dx - gutterWidth) / (fontSize / 2.0); var inMargin = (event.localPosition.dx - gutterWidth) > usableSize; var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy); var file = Provider.of(context, listen: false); @@ -92,7 +108,6 @@ class LineInfo extends ChangeNotifier { } }, onPointerMove: (event) { - //var c = (event.localPosition.dx - gutterWidth) / (fontSize / 2.0) var inMargin = (event.localPosition.dx - gutterWidth) > usableSize; var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy); var file = Provider.of(context, listen: false); @@ -104,7 +119,6 @@ class LineInfo extends ChangeNotifier { }, child: MouseRegion( onEnter: (event) { - //var c = (event.localPosition.dx - gutterWidth) / (fontSize / 2.0); var inMargin = (event.localPosition.dx - gutterWidth) > usableSize; var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy); var file = Provider.of(context, listen: false); @@ -115,7 +129,6 @@ class LineInfo extends ChangeNotifier { } }, onHover: (event) { - //var c = (event.localPosition.dx - gutterWidth) / (fontSize / 2.0); var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy); var file = Provider.of(context, listen: false); if (event.down) { @@ -145,16 +158,7 @@ class LineInfo extends ChangeNotifier { margin: EdgeInsets.only(right: 1, top: 0, bottom: 0, left: 0), alignment: Alignment.centerLeft, child: Flex(direction: Axis.horizontal, mainAxisAlignment: MainAxisAlignment.start, children: sidebarContent)), - Container( - padding: EdgeInsets.zero, - margin: EdgeInsets.only(right: rightMargin, top: 0, bottom: 0, left: 0), - decoration: BoxDecoration( - color: backgroundStyle.backgroundColor, - ), - width: constraints.maxWidth - rightMargin - gutterWidth, - height: idealHeight, - child: cursorOverlay, - ), + cursorOverlay, ])))); }, ); diff --git a/lib/main.dart b/lib/main.dart index 8780892..7e73a0e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,7 +8,9 @@ import 'outline.dart'; void main(List args) { var docfile = LineFile(); - docfile.openFile("./example.md", 0); + //if (args.length > 1) { + docfile.openFile("./example.md", 0); + //} var doc = ChangeNotifierProvider.value(value: docfile); runApp(MultiProvider( @@ -26,7 +28,11 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'subwrite', - theme: ThemeData(scrollbarTheme: ScrollbarThemeData(thumbColor: MaterialStateProperty.all(header))), + theme: ThemeData( + scrollbarTheme: ScrollbarThemeData( + thumbColor: MaterialStateProperty.all(header), + thumbVisibility: MaterialStateProperty.all(true), + )), home: const SarahDownApp(), ); } diff --git a/lib/outline.dart b/lib/outline.dart index 8384c97..dcc29ea 100644 --- a/lib/outline.dart +++ b/lib/outline.dart @@ -62,7 +62,7 @@ class _Outline extends State { style: TextStyle(color: foreground, fontSize: 14.0 - sections[index].level, fontFamily: "Iosevka"), ), onTap: () { - Provider.of(context, listen: false).scrollController.jumpTo(sections[index].lineNumber * 17.0); + Provider.of(context, listen: false).scrollController.jumpTo(index: sections[index].lineNumber - 1); }, trailing: ReorderableDragStartListener( index: index, diff --git a/lib/view.dart b/lib/view.dart index ad6ee0d..d43cc49 100644 --- a/lib/view.dart +++ b/lib/view.dart @@ -1,23 +1,32 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'file.dart'; import 'line_info.dart'; class View extends StatefulWidget { View({Key? key}) : super(key: key); + @override _View createState() => _View(); } -class _View extends State { +class _View extends State with SingleTickerProviderStateMixin { + + late AnimationController _animationController; + @override void initState() { super.initState(); + _animationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 300)); + _animationController.repeat(reverse: true); + } @override void dispose() { + _animationController.dispose(); super.dispose(); } @@ -25,22 +34,18 @@ class _View extends State { Widget build(BuildContext context) { LineFile doc = Provider.of(context); - return Scrollbar( - controller: doc.scrollController, - trackVisibility: true, - thumbVisibility: true, - child: ListView.builder( - physics: const AlwaysScrollableScrollPhysics(), - controller: doc.scrollController, - itemCount: doc.lines(), - padding: EdgeInsets.zero, - shrinkWrap: true, - itemBuilder: (BuildContext bcontext, int index) { - return ChangeNotifierProvider.value( - value: doc.backingLines[index], - builder: (lcontext, lineinfo) { - return Provider.of(lcontext).build(lcontext); - }); - })); + return ScrollablePositionedList.builder( + physics: const AlwaysScrollableScrollPhysics(), + itemScrollController: doc.scrollController, + itemCount: doc.lines(), + padding: EdgeInsets.zero, + shrinkWrap: true, + itemBuilder: (BuildContext bcontext, int index) { + return ChangeNotifierProvider.value( + value: doc.backingLines[index], + builder: (lcontext, lineinfo) { + return Provider.of(lcontext).build(context,_animationController); + }); + }); } } diff --git a/pubspec.lock b/pubspec.lock index 29872f3..7e313d6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -116,6 +116,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.0.3" + scrollable_positioned_list: + dependency: "direct main" + description: + name: scrollable_positioned_list + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" sky_engine: dependency: transitive description: flutter @@ -172,4 +179,4 @@ packages: version: "2.1.2" sdks: dart: ">=2.18.0 <3.0.0" - flutter: ">=1.16.0" + flutter: ">=2.12.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4677470..537af2a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 provider: ^6.0.2 + scrollable_positioned_list: ^0.3.2 dev_dependencies: flutter_test: