Initial Commit
This commit is contained in:
parent
996ef3e358
commit
da902700a9
|
@ -0,0 +1,44 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
|
@ -0,0 +1,30 @@
|
|||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled.
|
||||
|
||||
version:
|
||||
revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c
|
||||
channel: stable
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c
|
||||
base_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c
|
||||
- platform: linux
|
||||
create_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c
|
||||
base_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
|
@ -0,0 +1,29 @@
|
|||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at
|
||||
# https://dart-lang.github.io/linter/lints/index.html.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
Binary file not shown.
|
@ -0,0 +1,70 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:subwrite/sourcelink.dart';
|
||||
import 'package:subwrite/theme.dart';
|
||||
|
||||
enum AnnotationType {
|
||||
highlight,
|
||||
error,
|
||||
todo,
|
||||
note,
|
||||
}
|
||||
|
||||
class LineAnnotation {
|
||||
SourceLink sourceLink;
|
||||
AnnotationType annotationType;
|
||||
String type;
|
||||
SourceLink? reflink;
|
||||
String? description;
|
||||
|
||||
// the tool that generated this annotation
|
||||
String tool;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is LineAnnotation && other.annotationType == annotationType && other.description == description && other.sourceLink == sourceLink;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = Object.hash(sourceLink, annotationType, description);
|
||||
return hash;
|
||||
}
|
||||
|
||||
LineAnnotation(this.sourceLink, this.tool, this.annotationType, this.type, {this.reflink, this.description});
|
||||
|
||||
Icon getIcon(double size) {
|
||||
if (type == "note") {
|
||||
return Icon(
|
||||
Icons.note,
|
||||
color: literal,
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
if (type == "todo") {
|
||||
return Icon(
|
||||
Icons.details,
|
||||
color: variable,
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
if (type == "incomplete") {
|
||||
return Icon(
|
||||
Icons.label,
|
||||
color: constant,
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
if (type == "edit") {
|
||||
return Icon(
|
||||
Icons.label,
|
||||
color: literal,
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
return Icon(
|
||||
Icons.warning_amber_outlined,
|
||||
color: Colors.red,
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
class Cursor {
|
||||
Cursor({this.line = 0, this.column = 0, this.anchorLine = 0, this.anchorColumn = 0});
|
||||
|
||||
int line = 0;
|
||||
int column = 0;
|
||||
int anchorLine = 0;
|
||||
int anchorColumn = 0;
|
||||
|
||||
Cursor copy() {
|
||||
return Cursor(line: line, column: column, anchorLine: anchorLine, anchorColumn: anchorColumn);
|
||||
}
|
||||
|
||||
Cursor normalized() {
|
||||
Cursor res = copy();
|
||||
if (line > anchorLine || (line == anchorLine && column > anchorColumn)) {
|
||||
res.line = anchorLine;
|
||||
res.column = anchorColumn;
|
||||
res.anchorLine = line;
|
||||
res.anchorColumn = column;
|
||||
return res;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
bool hasSelection() {
|
||||
return line != anchorLine || column != anchorColumn;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'cursor.dart';
|
||||
import 'line_info.dart';
|
||||
|
||||
class CursorProvider extends ChangeNotifier {
|
||||
Cursor cursor = Cursor(line: 1, column: 0);
|
||||
bool inMouseSelection = false;
|
||||
|
||||
int get line => cursor.line;
|
||||
int get column => cursor.column;
|
||||
|
||||
void publishCursor(List<LineInfo> elements) {
|
||||
var ncursor = cursor.normalized();
|
||||
elements.forEach((element) {
|
||||
element.setCursor(ncursor);
|
||||
});
|
||||
}
|
||||
|
||||
void moveCursorToDocStart() {
|
||||
clearSelection();
|
||||
cursor.line = 0;
|
||||
cursor.column = 0;
|
||||
cursor = cursor.normalized();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void moveCursorToLineStart() {
|
||||
cursor.column = 0;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearSelection() {
|
||||
inMouseSelection = false;
|
||||
cursor.anchorLine = cursor.line;
|
||||
cursor.anchorColumn = cursor.column;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void moveCursorToLineEnd(int lineLength) {
|
||||
cursor.column = lineLength;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void moveCursor(int dx, int dy, List<LineInfo> backingLines, {bool keepAnchor = false}) {
|
||||
//print(cursor.line.toString() + " " + cursor.column.toString() + " " + dx.toString() + " " + dy.toString() + " " + backingLines[cursor.line - 1].text.length.toString());
|
||||
|
||||
if (dx != 0) {
|
||||
if (cursor.column + dx < 0) {
|
||||
moveCursor(0, -1, backingLines, keepAnchor: keepAnchor);
|
||||
if (cursor.line > 1) {
|
||||
moveCursorToLineEnd(backingLines[cursor.line - 1].text.length);
|
||||
}
|
||||
} else if (cursor.column + dx > backingLines[cursor.line - 1].text.length) {
|
||||
moveCursor(0, 1, backingLines, keepAnchor: keepAnchor);
|
||||
moveCursorToLineStart();
|
||||
} else {
|
||||
cursor.column += dx;
|
||||
}
|
||||
}
|
||||
|
||||
if (dy != 0) {
|
||||
// Only allow the cursor line to change if in bounds
|
||||
if (cursor.line + dy > 0 && cursor.line + dy <= backingLines.length) {
|
||||
cursor.line += dy;
|
||||
// Change cursor to line end
|
||||
if (cursor.column > backingLines[cursor.line - 1].text.length) {
|
||||
moveCursorToLineEnd(backingLines[cursor.line - 1].text.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!keepAnchor) {
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void mouseDownEnd() {
|
||||
inMouseSelection = false;
|
||||
}
|
||||
|
||||
void mouseDownMoveUpdate(int col, List<LineInfo> backingLines) {
|
||||
if (cursor.column != col) {
|
||||
var lineLength = backingLines[cursor.line - 1].text.length;
|
||||
cursor.column = col.clamp(0, lineLength);
|
||||
if (inMouseSelection == false) {
|
||||
clearSelection();
|
||||
}
|
||||
publishCursor(backingLines);
|
||||
notifyListeners();
|
||||
} else if (inMouseSelection == false) {
|
||||
clearSelection();
|
||||
publishCursor(backingLines);
|
||||
notifyListeners();
|
||||
}
|
||||
inMouseSelection = true;
|
||||
}
|
||||
|
||||
void mouseDownUpdate(int line, int col, List<LineInfo> backingLines) {
|
||||
if (cursor.line != line || cursor.column != col) {
|
||||
cursor.line = line;
|
||||
var lineLength = backingLines[cursor.line - 1].text.length;
|
||||
cursor.column = col.clamp(0, lineLength);
|
||||
if (inMouseSelection == false) {
|
||||
clearSelection();
|
||||
}
|
||||
publishCursor(backingLines);
|
||||
notifyListeners();
|
||||
} else if (inMouseSelection == false) {
|
||||
clearSelection();
|
||||
publishCursor(backingLines);
|
||||
notifyListeners();
|
||||
}
|
||||
inMouseSelection = true;
|
||||
}
|
||||
|
||||
bool hasSelection() {
|
||||
return cursor.hasSelection();
|
||||
}
|
||||
|
||||
Cursor normalized() {
|
||||
return cursor.normalized();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:subwrite/view.dart';
|
||||
|
||||
import 'input.dart';
|
||||
|
||||
class Editor extends StatefulWidget {
|
||||
const Editor({super.key});
|
||||
|
||||
@override
|
||||
State<Editor> createState() => _Editor();
|
||||
}
|
||||
|
||||
class _Editor extends State<Editor> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InputListener(LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) {
|
||||
//print("opening document " + path + " " + startLine.toString());
|
||||
return View();
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,472 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:subwrite/annotation.dart';
|
||||
|
||||
import 'package:subwrite/section.dart';
|
||||
import 'package:subwrite/sourcelink.dart';
|
||||
import 'package:subwrite/suggestion.dart';
|
||||
import 'cursor_provider.dart';
|
||||
import 'line_info.dart';
|
||||
|
||||
class LineFile extends ChangeNotifier {
|
||||
List<LineInfo> backingLines = List.empty(growable: true);
|
||||
|
||||
CursorProvider cursor = CursorProvider();
|
||||
FocusNode focus = FocusNode();
|
||||
bool showSuggestions = false;
|
||||
late String projectPath;
|
||||
late String path;
|
||||
|
||||
ScrollController scrollController = ScrollController();
|
||||
|
||||
void openFile(String path, int startLine) {
|
||||
this.path = path;
|
||||
File f = File(path);
|
||||
var line = 0;
|
||||
f.readAsLinesSync().forEach((text) {
|
||||
backingLines.insert(line, LineInfo(path, line + 1, text.replaceAll("\t", " ").trimRight()));
|
||||
line += 1;
|
||||
});
|
||||
|
||||
// handle the empty file case
|
||||
if (backingLines.isEmpty) {
|
||||
backingLines.insert(0, LineInfo(path, 1, ""));
|
||||
}
|
||||
parse();
|
||||
cursor.moveCursorToDocStart();
|
||||
focus.requestFocus();
|
||||
scrollController = ScrollController(initialScrollOffset: (startLine * 17.0 - (17.0 * 10.0)).clamp(0.0, double.infinity));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void cancelSuggestions() {
|
||||
if (showSuggestions) {
|
||||
suggestionIndex = 0;
|
||||
showSuggestions = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
ScrollController suggestionListController = ScrollController(initialScrollOffset: 0.0);
|
||||
int suggestionIndex = 0;
|
||||
List<Suggestion> suggestions = List.empty();
|
||||
|
||||
// Future<List<Suggestion>> loadSuggestions(IDE ide) async {
|
||||
// return ide.getSuggestions(projectPath, path, cursor.line, cursor.column, base64()).then((list) {
|
||||
// suggestions = list;
|
||||
// return list;
|
||||
// });
|
||||
// }
|
||||
|
||||
void suggestListUp() {
|
||||
suggestionIndex = (suggestionIndex - 1) % suggestions.length;
|
||||
if (suggestions.length - suggestionIndex >= 14 || suggestionIndex == 0 || suggestionIndex == suggestions.length - 1) {
|
||||
suggestionListController.animateTo(suggestionIndex * 13.0, duration: const Duration(milliseconds: 100), curve: Curves.decelerate);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void suggestListDown() {
|
||||
suggestionIndex = (suggestionIndex + 1) % suggestions.length;
|
||||
if (suggestions.length - suggestionIndex >= 14 || suggestionIndex == 0 || suggestionIndex == suggestions.length - 1) {
|
||||
suggestionListController.animateTo(suggestionIndex * 13.0, duration: const Duration(milliseconds: 100), curve: Curves.decelerate);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void suggestListInsert() {
|
||||
suggestionIndex = (suggestionIndex) % suggestions.length;
|
||||
for (var char = suggestions[suggestionIndex].partialLen; char < suggestions[suggestionIndex].name.length; char++) {
|
||||
insertChar(suggestions[suggestionIndex].name[char]);
|
||||
}
|
||||
cursor.publishCursor(backingLines);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
KeyEventResult handleKey(RawKeyEvent event) {
|
||||
// only process key down events...
|
||||
if (event.runtimeType.toString() == 'RawKeyDownEvent') {
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
||||
cursor.moveCursor(-1, 0, backingLines, keepAnchor: event.isShiftPressed);
|
||||
cursor.publishCursor(backingLines);
|
||||
cancelSuggestions();
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
||||
cursor.moveCursor(1, 0, backingLines, keepAnchor: event.isShiftPressed);
|
||||
cursor.publishCursor(backingLines);
|
||||
cancelSuggestions();
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
||||
if (showSuggestions == false) {
|
||||
cursor.moveCursor(0, -1, backingLines, keepAnchor: event.isShiftPressed);
|
||||
cursor.publishCursor(backingLines);
|
||||
} else {
|
||||
suggestListUp();
|
||||
}
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||
if (showSuggestions == false) {
|
||||
cursor.moveCursor(0, 1, backingLines, keepAnchor: event.isShiftPressed);
|
||||
cursor.publishCursor(backingLines);
|
||||
} else {
|
||||
suggestListDown();
|
||||
}
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.home) {
|
||||
cancelSuggestions();
|
||||
cursor.clearSelection();
|
||||
cursor.moveCursorToLineStart();
|
||||
cursor.publishCursor(backingLines);
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.end) {
|
||||
cancelSuggestions();
|
||||
cursor.clearSelection();
|
||||
cursor.moveCursorToLineEnd(backingLines[cursor.line - 1].text.length);
|
||||
cursor.publishCursor(backingLines);
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.enter) {
|
||||
if (showSuggestions == false) {
|
||||
insertLine();
|
||||
cancelSuggestions();
|
||||
cursor.clearSelection();
|
||||
cursor.publishCursor(backingLines);
|
||||
} else {
|
||||
suggestListInsert();
|
||||
}
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.tab) {
|
||||
insertChar(" ");
|
||||
cancelSuggestions();
|
||||
cursor.publishCursor(backingLines);
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.delete) {
|
||||
handleDelete();
|
||||
cancelSuggestions();
|
||||
cursor.clearSelection();
|
||||
cursor.publishCursor(backingLines);
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
||||
handleBackspace();
|
||||
cancelSuggestions();
|
||||
cursor.clearSelection();
|
||||
cursor.publishCursor(backingLines);
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
showSuggestions = false;
|
||||
notifyListeners();
|
||||
} else {
|
||||
if (event.isControlPressed) {
|
||||
if (event.logicalKey == LogicalKeyboardKey.space) {
|
||||
showSuggestions = true;
|
||||
notifyListeners();
|
||||
}
|
||||
if (event.character != null && event.character == 's') {
|
||||
saveFile();
|
||||
notifyListeners();
|
||||
}
|
||||
if (event.character != null && event.character == 'c') {
|
||||
copy();
|
||||
}
|
||||
if (event.character != null && event.character == 'v') {
|
||||
paste();
|
||||
notifyListeners();
|
||||
}
|
||||
} else {
|
||||
if (event.logicalKey == LogicalKeyboardKey.space) {
|
||||
insertChar(" ");
|
||||
} else if (event.character != null) {
|
||||
insertChar(event.character!);
|
||||
}
|
||||
}
|
||||
cursor.publishCursor(backingLines);
|
||||
}
|
||||
}
|
||||
parse();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
void saveFile() {
|
||||
String content = '';
|
||||
for (var l in backingLines) {
|
||||
content += l.text + '\n';
|
||||
}
|
||||
File(path).writeAsStringSync(content);
|
||||
}
|
||||
|
||||
void handleDelete() {
|
||||
if (cursor.hasSelection()) {
|
||||
deleteSelected();
|
||||
return;
|
||||
}
|
||||
|
||||
// if at the end of the line then we need to move the next line onto
|
||||
// our line, and delete what remains
|
||||
if (cursor.column == backingLines[cursor.line - 1].text.length) {
|
||||
backingLines[cursor.line - 1].text += backingLines[cursor.line].text;
|
||||
backingLines[cursor.line - 1].notifyListeners();
|
||||
backingLines.removeAt(cursor.line);
|
||||
renumberFrom(cursor.line);
|
||||
} else {
|
||||
// otherwise simply delete a char
|
||||
var beforeDeletedChar = backingLines[cursor.line - 1].text.substring(0, cursor.column);
|
||||
var afterDeletedChar = backingLines[cursor.line - 1].text.substring(cursor.column + 1);
|
||||
backingLines[cursor.line - 1].text = beforeDeletedChar + afterDeletedChar;
|
||||
backingLines[cursor.line - 1].notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void deleteSelected() {
|
||||
var ncursor = cursor.normalized();
|
||||
if (ncursor.hasSelection()) {
|
||||
if (ncursor.anchorLine == ncursor.line) {
|
||||
// simple case, just trim column
|
||||
var beforeDeletedChar = backingLines[ncursor.line - 1].text.substring(0, ncursor.column);
|
||||
var afterDeletedChar = backingLines[ncursor.line - 1].text.substring(ncursor.anchorColumn);
|
||||
backingLines[ncursor.line - 1].text = beforeDeletedChar + afterDeletedChar;
|
||||
backingLines[ncursor.line - 1].notifyListeners();
|
||||
cursor.clearSelection();
|
||||
} else {
|
||||
// First Line
|
||||
var withDeletedRemainder = backingLines[ncursor.line - 1].text.substring(0, ncursor.column);
|
||||
var withDeletedPrefix = backingLines[ncursor.anchorLine - 1].text.substring(ncursor.anchorColumn);
|
||||
backingLines[ncursor.line - 1].text = withDeletedRemainder + withDeletedPrefix;
|
||||
|
||||
// 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);
|
||||
cursor.clearSelection();
|
||||
}
|
||||
}
|
||||
cursor.clearSelection();
|
||||
}
|
||||
|
||||
void handleBackspace() {
|
||||
if (cursor.hasSelection()) {
|
||||
deleteSelected();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cursor.column > 0) {
|
||||
var beforeDeletedChar = backingLines[cursor.line - 1].text.substring(0, cursor.column - 1);
|
||||
var afterDeletedChar = backingLines[cursor.line - 1].text.substring(cursor.column);
|
||||
backingLines[cursor.line - 1].text = beforeDeletedChar + afterDeletedChar;
|
||||
backingLines[cursor.line - 1].notifyListeners();
|
||||
cursor.moveCursor(-1, 0, backingLines);
|
||||
} else if (cursor.line > 1) {
|
||||
// join current line to previous line
|
||||
cursor.moveCursor(0, -1, backingLines);
|
||||
cursor.moveCursorToLineEnd(backingLines[cursor.line - 1].text.length);
|
||||
backingLines[cursor.line - 1].text += backingLines[cursor.line].text;
|
||||
backingLines[cursor.line - 1].notifyListeners();
|
||||
// delete current line
|
||||
backingLines.removeAt(cursor.line);
|
||||
renumberFrom(cursor.line);
|
||||
// renumber
|
||||
}
|
||||
}
|
||||
|
||||
void insertChar(String ch) {
|
||||
var beforeInsertedChar = backingLines[cursor.line - 1].text.substring(0, cursor.column);
|
||||
var afterInsertedChar = backingLines[cursor.line - 1].text.substring(cursor.column);
|
||||
backingLines[cursor.line - 1].text = beforeInsertedChar + ch + afterInsertedChar;
|
||||
cursor.moveCursor(ch.length, 0, backingLines);
|
||||
backingLines[cursor.line - 1].notify();
|
||||
}
|
||||
|
||||
void insertLine() {
|
||||
// get the content that will be moved to the next line
|
||||
var restOfLine = backingLines[cursor.line - 1].text.substring(cursor.column);
|
||||
// split off the line after the cursor
|
||||
backingLines[cursor.line - 1].text = backingLines[cursor.line - 1].text.substring(0, cursor.column);
|
||||
// insert a line directly into this position
|
||||
backingLines.insert(cursor.line, LineInfo(path, cursor.line + 1, restOfLine));
|
||||
// Move the Cursor Down
|
||||
cursor.moveCursor(0, 1, backingLines);
|
||||
// renumber all the lines afterwards
|
||||
renumberFrom(cursor.line - 1);
|
||||
cursor.moveCursorToLineStart();
|
||||
}
|
||||
|
||||
void renumberFrom(int startLine) {
|
||||
for (int line = startLine; line < backingLines.length; line++) {
|
||||
backingLines[line].lineNumber = line + 1;
|
||||
backingLines[line].notifyListeners();
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
int lines() {
|
||||
return backingLines.length;
|
||||
}
|
||||
|
||||
final _boldRegex = RegExp(
|
||||
r'(\*\*([^*]*)\*\*)',
|
||||
caseSensitive: false,
|
||||
dotAll: true,
|
||||
);
|
||||
|
||||
final _codeRegex = RegExp(
|
||||
r'(\`([^`]*)\`)',
|
||||
caseSensitive: false,
|
||||
dotAll: true,
|
||||
);
|
||||
|
||||
List<Section> sections = List.empty(growable: true);
|
||||
|
||||
// rebuild the annotations manager
|
||||
void parse() {
|
||||
|
||||
sections.clear();
|
||||
|
||||
for (var i = 0; i < backingLines.length; i++) {
|
||||
var line = backingLines[i];
|
||||
|
||||
backingLines[i].annotations.removeWhere((element) => element.tool == "markdown");
|
||||
|
||||
if (line.text.startsWith("//")) {
|
||||
backingLines[i].annotations.add(
|
||||
LineAnnotation(SourceLink(path, lineStart: backingLines[i].lineNumber, colStart: 0, colEnd: line.text.length + 1, lineEnd: i), "markdown", AnnotationType.highlight, "comment"));
|
||||
|
||||
if (line.text.contains("TODO")) {
|
||||
backingLines[i].annotations.add(
|
||||
LineAnnotation(SourceLink(path, lineStart: backingLines[i].lineNumber, colStart: 0, colEnd: line.text.length + 1, lineEnd: i), "markdown", AnnotationType.todo, "todo"));
|
||||
}
|
||||
}
|
||||
|
||||
if (line.text.startsWith("#")) {
|
||||
// this is a header
|
||||
backingLines[i].annotations.add(
|
||||
LineAnnotation(SourceLink(path, lineStart: backingLines[i].lineNumber, colStart: 0, colEnd: line.text.length + 1, lineEnd: i), "markdown", AnnotationType.highlight, "header"));
|
||||
|
||||
var level = 0;
|
||||
for (var lvl = 0; lvl < 6; lvl++) {
|
||||
if (line.text.length > lvl) {
|
||||
if (line.text[lvl] == '#') {
|
||||
level += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sections.add(Section(backingLines[i].lineNumber, level, line.text.substring(level + 1)));
|
||||
}
|
||||
|
||||
if (line.text.startsWith(" ")) {
|
||||
backingLines[i].annotations.add(
|
||||
LineAnnotation(SourceLink(path, lineStart: backingLines[i].lineNumber, colStart: 0, colEnd: line.text.length + 1, lineEnd: i), "markdown", AnnotationType.highlight, "code"));
|
||||
}
|
||||
|
||||
_boldRegex.allMatches(backingLines[i].text).forEach((match) {
|
||||
backingLines[i].annotations.add(
|
||||
LineAnnotation(SourceLink(path, lineStart: backingLines[i].lineNumber, colStart: match.start, colEnd: match.end + 1, lineEnd: i), "markdown", AnnotationType.highlight, "bold"));
|
||||
});
|
||||
|
||||
_codeRegex.allMatches(backingLines[i].text).forEach((match) {
|
||||
backingLines[i].annotations.add(
|
||||
LineAnnotation(SourceLink(path, lineStart: backingLines[i].lineNumber, colStart: match.start, colEnd: match.end + 1, lineEnd: i), "markdown", AnnotationType.highlight, "code"));
|
||||
});
|
||||
}
|
||||
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";
|
||||
for (var line = ncursor.line + 1; line < ncursor.anchorLine; line++) {
|
||||
firstLine += backingLines[line - 1].text + "\n";
|
||||
}
|
||||
var textToCopy = firstLine;
|
||||
if (ncursor.anchorLine != ncursor.line) {
|
||||
var lastLine = backingLines[ncursor.anchorLine - 1].text.substring(0, ncursor.anchorColumn);
|
||||
textToCopy += lastLine;
|
||||
}
|
||||
Clipboard.setData(ClipboardData(text: textToCopy));
|
||||
}
|
||||
|
||||
void paste() {
|
||||
deleteSelected(); // if we have a selection we need to delete it before pasting new text
|
||||
Clipboard.getData("text/plain").then((value) {
|
||||
if (value != null && value.text != null) {
|
||||
for (var i = 0; i < value.text!.length; i++) {
|
||||
var char = value.text![i];
|
||||
if (char == "\n") {
|
||||
insertLine();
|
||||
} else {
|
||||
insertChar(char);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void mouseDownUpdate(int line, int col) {
|
||||
cancelSuggestions();
|
||||
cursor.mouseDownUpdate(line, col, backingLines);
|
||||
}
|
||||
|
||||
void mouseDownMoveUpdate(int col) {
|
||||
cancelSuggestions();
|
||||
cursor.mouseDownMoveUpdate(col, backingLines);
|
||||
}
|
||||
|
||||
void mouseDownEnd() {
|
||||
cursor.mouseDownEnd();
|
||||
}
|
||||
|
||||
void moveLines(int startLine, int endLine, int newLine) {
|
||||
print("want to move from $startLine (: $endLine) to $newLine\n");
|
||||
List<LineInfo> insert = List.empty(growable: true);
|
||||
|
||||
// backup the section
|
||||
for (int line = startLine; line < endLine; line++) {
|
||||
insert.add(backingLines[line - 1]);
|
||||
}
|
||||
|
||||
// remove the section
|
||||
for (int line = startLine; line < endLine; line++) {
|
||||
backingLines.removeAt(startLine - 1);
|
||||
}
|
||||
|
||||
// if we want to put a line that was on e.g. 5 on 10 then we have to remove 5...
|
||||
// (unlike if it was on 10 and we want to put it on 5...it hasn't changed)
|
||||
if (newLine > startLine) {
|
||||
newLine -= endLine - startLine;
|
||||
}
|
||||
|
||||
// reinsert
|
||||
for (int line = 0; line < insert.length; line++) {
|
||||
backingLines.insert(newLine - 1 + line, insert[line]);
|
||||
}
|
||||
|
||||
renumberFrom(0);
|
||||
parse();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
addNote(String note) {
|
||||
|
||||
var notetype = "note";
|
||||
if (note.startsWith("[incomplete]")) {
|
||||
notetype = "incomplete";
|
||||
} else if (note.startsWith("[edit]")) {
|
||||
notetype = "edit";
|
||||
}
|
||||
|
||||
backingLines[cursor.line-1].annotations.add(
|
||||
LineAnnotation(
|
||||
SourceLink(
|
||||
path,
|
||||
lineStart: cursor.line,
|
||||
),
|
||||
"notes",
|
||||
AnnotationType.note,
|
||||
notetype,
|
||||
description: note));
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:subwrite/sourcelink.dart';
|
||||
import 'package:subwrite/theme.dart';
|
||||
|
||||
import 'annotation.dart';
|
||||
|
||||
double fontSize = 14;
|
||||
double gutterFontSize = 12;
|
||||
|
||||
class LineDecoration {
|
||||
int start = 0;
|
||||
int end = 0;
|
||||
Color color = Colors.white;
|
||||
Color background = Colors.white;
|
||||
bool underline = false;
|
||||
bool italic = false;
|
||||
bool bold = false;
|
||||
|
||||
SourceLink? message;
|
||||
}
|
||||
|
||||
class Highlighter {
|
||||
Highlighter() {}
|
||||
|
||||
List<InlineSpan> run(BuildContext context, String text, int line, bool highlightRisk, TextStyle style, List<LineAnnotation>? insights, int? cursorStart, int? cursorEnd, bool lineSelected) {
|
||||
TextStyle defaultStyle = style.copyWith(fontFamily: 'Iosevka', fontSize: fontSize, color: foreground, height: 1.2);
|
||||
List<InlineSpan> res = <InlineSpan>[];
|
||||
List<LineDecoration> decors = <LineDecoration>[];
|
||||
|
||||
insights
|
||||
?.where(
|
||||
(element) => element.annotationType == AnnotationType.highlight,
|
||||
)
|
||||
.forEach((element) {
|
||||
LineDecoration d = LineDecoration();
|
||||
//print("${element.sourceLink.colStart} ${element.sourceLink.colEnd}");
|
||||
d.start = element.sourceLink.colStart! - 1;
|
||||
d.end = element.sourceLink.colEnd! - 2;
|
||||
d.background = Colors.transparent;
|
||||
switch (element.type) {
|
||||
case "header":
|
||||
d.color = header;
|
||||
break;
|
||||
case "bold":
|
||||
d.color = keyword;
|
||||
d.bold = true;
|
||||
break;
|
||||
case "code":
|
||||
d.color = function;
|
||||
break;
|
||||
}
|
||||
|
||||
decors.add(d);
|
||||
});
|
||||
|
||||
// Assume that decorators do not overlap...
|
||||
decors.sort((a, b) {
|
||||
return a.start.compareTo(b.start);
|
||||
});
|
||||
|
||||
text += ' ';
|
||||
|
||||
for (int i = 0; i < text.length; i++) {
|
||||
String current = text[i];
|
||||
TextStyle style = defaultStyle.copyWith();
|
||||
|
||||
// decorate
|
||||
for (var d in decors) {
|
||||
// if we are in this decoration...
|
||||
if (i >= d.start && i <= d.end) {
|
||||
style = style.copyWith(color: d.color);
|
||||
if (d.bold = true) {
|
||||
style = style.copyWith(fontWeight: FontWeight.bold);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// is within selection
|
||||
if (lineSelected) {
|
||||
style = style.copyWith(backgroundColor: selection);
|
||||
} else if (cursorStart != null && cursorEnd != null) {
|
||||
if (i >= cursorStart && i < cursorEnd) {
|
||||
style = style.copyWith(backgroundColor: selection);
|
||||
}
|
||||
} else if (cursorStart != null && cursorEnd == null) {
|
||||
if (i >= cursorStart) {
|
||||
style = style.copyWith(backgroundColor: selection);
|
||||
}
|
||||
} else if (cursorStart == null && cursorEnd != null) {
|
||||
if (i < cursorEnd) {
|
||||
style = style.copyWith(backgroundColor: selection);
|
||||
}
|
||||
}
|
||||
|
||||
if (highlightRisk) {
|
||||
style = style.copyWith(
|
||||
backgroundColor: risk.withOpacity(0.50),
|
||||
);
|
||||
}
|
||||
|
||||
if (current == " ") {
|
||||
res.add(TextSpan(text: "_", style: style.copyWith(color: style.backgroundColor), semanticsLabel: " ", mouseCursor: SystemMouseCursors.text));
|
||||
} else {
|
||||
// we need to add a '\ufeff' (Zero Width No-Break Space) here to avoid RichText doing ridiculous
|
||||
// breaking logic with hyphens / slashes etc.
|
||||
res.add(TextSpan(text: '$current\ufeff', style: style, mouseCursor: SystemMouseCursors.text));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:subwrite/theme.dart';
|
||||
|
||||
import 'file.dart';
|
||||
|
||||
class InputListener extends StatefulWidget {
|
||||
late Widget child;
|
||||
InputListener(this.child);
|
||||
@override
|
||||
_InputListener createState() => _InputListener();
|
||||
}
|
||||
|
||||
class _InputListener extends State<InputListener> {
|
||||
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) {
|
||||
// show the dialog
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
TextEditingController controller = TextEditingController();
|
||||
return AlertDialog(
|
||||
shape: ContinuousRectangleBorder(side: BorderSide(color: sidebarAlt)),
|
||||
backgroundColor: sidebar,
|
||||
title: Align(alignment: Alignment.topCenter, child: Text(title)),
|
||||
titleTextStyle: TextStyle(fontSize: 10.0),
|
||||
titlePadding: EdgeInsets.all(5.0),
|
||||
content: TextField(
|
||||
autofocus: true,
|
||||
controller: controller,
|
||||
onSubmitted: (newValue) {
|
||||
callback(newValue);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
style: TextStyle(color: Colors.white, fontSize: 10.0),
|
||||
cursorColor: Colors.white,
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: TextStyle(color: Colors.white38),
|
||||
isDense: true,
|
||||
fillColor: sidebarHighlight,
|
||||
focusColor: sidebarHighlight,
|
||||
labelStyle: TextStyle(color: Colors.white),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(width: 1, color: sidebarAlt),
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(width: 1, color: sidebarHighlight),
|
||||
borderRadius: BorderRadius.zero,
|
||||
)),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 1.0, horizontal: 5.0),
|
||||
actions: [
|
||||
|
||||
ElevatedButton.icon(icon: Icon(Icons.label, color: literal,), onPressed: () {
|
||||
controller.text = "[edit]${controller.text}";
|
||||
callback(controller.text);
|
||||
Navigator.of(context).pop();
|
||||
}, label: Text("Edit"),style: ButtonStyle(backgroundColor: MaterialStateProperty.all(tabs), foregroundColor: MaterialStateProperty.all(foreground))),
|
||||
|
||||
|
||||
ElevatedButton.icon(icon: Icon(Icons.label, color: constant,), onPressed: () {
|
||||
controller.text = "[incomplete]${controller.text}";
|
||||
callback(controller.text);
|
||||
Navigator.of(context).pop();
|
||||
}, label: Text("Incomplete"),style: ButtonStyle(backgroundColor: MaterialStateProperty.all(tabs), foregroundColor: MaterialStateProperty.all(foreground))),
|
||||
],
|
||||
);
|
||||
},
|
||||
).then((value) => null);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!focusNode.hasFocus) {
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
|
||||
LineFile doc = Provider.of<LineFile>(context);
|
||||
|
||||
return GestureDetector(
|
||||
child: Focus(
|
||||
child: widget.child,
|
||||
focusNode: focusNode,
|
||||
autofocus: true,
|
||||
onKey: (FocusNode node, RawKeyEvent event) {
|
||||
doc.handleKey(event);
|
||||
if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
newTextDialog(context, "New Note", "Note", (note) => {doc.addNote(note)});
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,251 @@
|
|||
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';
|
||||
|
||||
double gutterWidth = 64;
|
||||
double rightMargin = 32;
|
||||
|
||||
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) {
|
||||
bool highlightRisk = false;
|
||||
|
||||
var backgroundStyle = TextStyle(backgroundColor: cursorStart != null ? sidebarHighlight : background);
|
||||
|
||||
Highlighter hl = Highlighter();
|
||||
List<InlineSpan> spans = hl.run(context, text, lineNumber, highlightRisk, backgroundStyle, annotations, cursorStart, cursorEnd, lineSelected);
|
||||
|
||||
final gutterStyle = TextStyle(fontFamily: 'Iosevka', decoration: null, decorationColor: null, fontSize: gutterFontSize, color: comment, backgroundColor: sidebarAlt, height: 1.0);
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
var highlightedLine = RichText(
|
||||
text: TextSpan(
|
||||
children: spans,
|
||||
style: backgroundStyle,
|
||||
),
|
||||
);
|
||||
|
||||
List<Widget> 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 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: 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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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<LineFile>(context, listen: false);
|
||||
if (event.down && !inMargin) {
|
||||
file.mouseDownUpdate(lineNumber, c.round());
|
||||
}
|
||||
},
|
||||
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<LineFile>(context, listen: false);
|
||||
if (event.down && !inMargin) {
|
||||
file.mouseDownMoveUpdate(c.round());
|
||||
} else {
|
||||
file.mouseDownEnd();
|
||||
}
|
||||
},
|
||||
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<LineFile>(context, listen: false);
|
||||
if (event.down && !inMargin) {
|
||||
file.mouseDownUpdate(lineNumber, c.round());
|
||||
} else {
|
||||
file.mouseDownEnd();
|
||||
}
|
||||
},
|
||||
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<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) {
|
||||
notifyListeners();
|
||||
},
|
||||
child: Container(
|
||||
color: backgroundStyle.backgroundColor,
|
||||
padding: EdgeInsets.zero,
|
||||
margin: EdgeInsets.zero,
|
||||
height: idealHeight,
|
||||
width: constraints.maxWidth,
|
||||
child: Row(mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Container(
|
||||
padding: EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
color: sidebarAlt,
|
||||
),
|
||||
width: gutterWidth - 1,
|
||||
height: idealHeight,
|
||||
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,
|
||||
),
|
||||
]))));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:subwrite/file.dart';
|
||||
import 'package:subwrite/theme.dart';
|
||||
|
||||
import 'editor.dart';
|
||||
import 'outline.dart';
|
||||
|
||||
void main(List<String> args) {
|
||||
var docfile = LineFile();
|
||||
docfile.openFile("./example.md", 0);
|
||||
var doc = ChangeNotifierProvider.value(value: docfile);
|
||||
|
||||
|
||||
runApp(MultiProvider(
|
||||
providers: [doc],
|
||||
builder: (context, child) {
|
||||
return MyApp();
|
||||
}));
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'subwrite',
|
||||
theme: ThemeData(scrollbarTheme: ScrollbarThemeData(thumbColor: MaterialStateProperty.all(header))),
|
||||
home: const SarahDownApp(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SarahDownApp extends StatefulWidget {
|
||||
const SarahDownApp({super.key});
|
||||
|
||||
@override
|
||||
State<SarahDownApp> createState() => _SarahDownApp();
|
||||
}
|
||||
|
||||
class _SarahDownApp extends State<SarahDownApp> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: sidebarAlt,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [Expanded(child: Outline()), Container(color: comment)])),
|
||||
Flexible(flex: 5, child: Editor())
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:subwrite/file.dart';
|
||||
import 'package:subwrite/theme.dart';
|
||||
|
||||
class Outline extends StatefulWidget {
|
||||
const Outline({super.key});
|
||||
|
||||
@override
|
||||
State<Outline> createState() => _Outline();
|
||||
}
|
||||
|
||||
class _Outline extends State<Outline> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var sections = Provider.of<LineFile>(context).sections;
|
||||
|
||||
return Material(
|
||||
color: tabs,
|
||||
child: ReorderableListView.builder(
|
||||
header: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0),
|
||||
child: Column(children: [Row(mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Icon(
|
||||
Icons.edit,
|
||||
color: foreground,
|
||||
),
|
||||
Text("subwrite", style: TextStyle(fontSize: 16.0, color: foreground, fontWeight: FontWeight.bold, fontFamily: "Iosevka"))
|
||||
]), Divider(color: foreground,)])),
|
||||
footer: Padding(padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 10.0), child: Divider(color: foreground, thickness: 4.0)),
|
||||
onReorder: (int oldIndex, int newIndex) {
|
||||
var doc = Provider.of<LineFile>(context, listen: false);
|
||||
var startLine = sections[oldIndex].lineNumber;
|
||||
|
||||
var endLine = doc.lines();
|
||||
for (var index = oldIndex + 1; index < sections.length; index++) {
|
||||
print("${sections[oldIndex].level} > ${sections[index].level}");
|
||||
if (sections[oldIndex].level < sections[index].level) {
|
||||
// sub section...skipping
|
||||
} else {
|
||||
endLine = sections[index].lineNumber;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var newLine = newIndex >= sections.length ? doc.lines() : sections[newIndex].lineNumber;
|
||||
|
||||
doc.moveLines(startLine, endLine, newLine);
|
||||
},
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return ListTile(
|
||||
key: ValueKey(index),
|
||||
tileColor: tabs,
|
||||
title: Text(
|
||||
sections[index].title.padLeft(sections[index].title.length + sections[index].level - 1, ' '),
|
||||
style: TextStyle(color: foreground, fontSize: 14.0 - sections[index].level, fontFamily: "Iosevka"),
|
||||
),
|
||||
onTap: () {
|
||||
Provider.of<LineFile>(context, listen: false).scrollController.jumpTo(sections[index].lineNumber * 17.0);
|
||||
},
|
||||
trailing: ReorderableDragStartListener(
|
||||
index: index,
|
||||
child: Icon(
|
||||
Icons.drag_handle,
|
||||
color: foreground,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: sections.length,
|
||||
));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
class Section {
|
||||
int lineNumber;
|
||||
int level;
|
||||
String title;
|
||||
|
||||
Section(this.lineNumber, this.level, this.title);
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
class SourceLink {
|
||||
late final String file;
|
||||
final int? lineStart;
|
||||
final int? colStart;
|
||||
final int? lineEnd;
|
||||
final int? colEnd;
|
||||
|
||||
SourceLink(this.file, {this.lineStart, this.colStart, this.lineEnd, this.colEnd});
|
||||
|
||||
String lineLink() {
|
||||
return file + ":" + lineStart.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is SourceLink && other.hashCode == hashCode;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = Object.hash(file, lineStart, colStart, lineEnd, colEnd);
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
SourceLink? parseSourceLink(String line) {
|
||||
String? file;
|
||||
int? lineStart;
|
||||
int? colStart;
|
||||
int? lineEnd;
|
||||
int? colEnd;
|
||||
var parts = line.split(":");
|
||||
if (parts.isNotEmpty && parts[0].isNotEmpty) {
|
||||
file = parts[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
if (parts.length > 1 && parts[1].trim().isNotEmpty) {
|
||||
lineStart = int.parse(parts[1]);
|
||||
}
|
||||
if (parts.length > 2 && parts[2].trim().isNotEmpty) {
|
||||
colStart = int.parse(parts[2]);
|
||||
}
|
||||
if (parts.length > 3 && parts[3].trim().isNotEmpty) {
|
||||
lineEnd = int.parse(parts[3]);
|
||||
}
|
||||
if (parts.length > 4 && parts[4].trim().isNotEmpty) {
|
||||
colEnd = int.parse(parts[4]);
|
||||
}
|
||||
return SourceLink(file, lineStart: lineStart, lineEnd: lineEnd, colStart: colStart, colEnd: colEnd);
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:subwrite/theme.dart';
|
||||
|
||||
class Suggestion {
|
||||
String classType;
|
||||
String name;
|
||||
String typeSignature;
|
||||
int partialLen;
|
||||
|
||||
Suggestion(this.classType, this.name, this.partialLen, this.typeSignature);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is Suggestion && hashCode == other.hashCode;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => classType.hashCode + name.hashCode + typeSignature.hashCode;
|
||||
|
||||
Widget getWidget() {
|
||||
var color = function;
|
||||
switch (classType) {
|
||||
case "const":
|
||||
color = constant;
|
||||
break;
|
||||
case "var":
|
||||
color = variable;
|
||||
break;
|
||||
case "package":
|
||||
color = keyword;
|
||||
break;
|
||||
case "type":
|
||||
color = header;
|
||||
break;
|
||||
}
|
||||
|
||||
return Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
RichText(text: TextSpan(text: name, style: TextStyle(color: color, fontFamily: "Iosevka", fontSize: 11.0, fontWeight: FontWeight.bold))),
|
||||
RichText(text: TextSpan(text: typeSignature, style: const TextStyle(fontFamily: "Iosevka", fontSize: 10.0))),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import 'dart:ui';
|
||||
|
||||
Color foreground = const Color(0xfff8f8f2);
|
||||
Color background = const Color(0xff000000); //Color(0xFF1A0F1C);
|
||||
Color comment = const Color(0xffd684e8);
|
||||
Color selection = const Color(0xff777889);
|
||||
|
||||
Color constant = const Color(0xff8be9fd);
|
||||
Color header = const Color(0xffbd93f9);
|
||||
Color keyword = const Color(0xffffb86c);
|
||||
Color variable = const Color(0xffff79c6);
|
||||
Color function = const Color(0xfff1fa8c);
|
||||
Color literal = const Color(0xff50fa7b);
|
||||
Color field = header;
|
||||
|
||||
Color risk = const Color(0xff45353b);
|
||||
|
||||
Color sidebarAlt = const Color(0xFF110F15);
|
||||
Color sidebar = const Color(0xff202023);
|
||||
Color tabs = const Color(0xFF342036);
|
||||
Color sidebarHighlight = const Color(0xFF38213D);
|
|
@ -0,0 +1,46 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.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<View> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
LineFile doc = Provider.of<LineFile>(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<LineInfo>(lcontext).build(lcontext);
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
flutter/ephemeral
|
|
@ -0,0 +1,138 @@
|
|||
# Project-level configuration.
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
# The name of the executable created for the application. Change this to change
|
||||
# the on-disk name of your application.
|
||||
set(BINARY_NAME "sarahdown")
|
||||
# The unique GTK application identifier for this application. See:
|
||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||
set(APPLICATION_ID "com.example.sarahdown")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
cmake_policy(SET CMP0063 NEW)
|
||||
|
||||
# Load bundled libraries from the lib/ directory relative to the binary.
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
|
||||
|
||||
# Root filesystem for cross-building.
|
||||
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
|
||||
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
endif()
|
||||
|
||||
# Define build configuration options.
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||
STRING "Flutter build mode" FORCE)
|
||||
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||
"Debug" "Profile" "Release")
|
||||
endif()
|
||||
|
||||
# Compilation settings that should be applied to most targets.
|
||||
#
|
||||
# Be cautious about adding new options here, as plugins use this function by
|
||||
# default. In most cases, you should add new options to specific targets instead
|
||||
# of modifying this function.
|
||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||
endfunction()
|
||||
|
||||
# Flutter library and tool build rules.
|
||||
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||
|
||||
# System-level dependencies.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
|
||||
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||
|
||||
# Define the application target. To change its name, change BINARY_NAME above,
|
||||
# not the value here, or `flutter run` will no longer work.
|
||||
#
|
||||
# Any new source files that you add to the application should be added here.
|
||||
add_executable(${BINARY_NAME}
|
||||
"main.cc"
|
||||
"my_application.cc"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
)
|
||||
|
||||
# Apply the standard set of build settings. This can be removed for applications
|
||||
# that need different build settings.
|
||||
apply_standard_settings(${BINARY_NAME})
|
||||
|
||||
# Add dependency libraries. Add any application-specific dependencies here.
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
|
||||
|
||||
# Run the Flutter tool portions of the build. This must not be removed.
|
||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||
|
||||
# Only the install-generated bundle's copy of the executable will launch
|
||||
# correctly, since the resources must in the right relative locations. To avoid
|
||||
# people trying to run the unbundled copy, put it in a subdirectory instead of
|
||||
# the default top-level location.
|
||||
set_target_properties(${BINARY_NAME}
|
||||
PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
|
||||
)
|
||||
|
||||
# Generated plugin build rules, which manage building the plugins and adding
|
||||
# them to the application.
|
||||
include(flutter/generated_plugins.cmake)
|
||||
|
||||
|
||||
# === Installation ===
|
||||
# By default, "installing" just makes a relocatable bundle in the build
|
||||
# directory.
|
||||
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||
endif()
|
||||
|
||||
# Start with a clean build bundle directory every time.
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
|
||||
" COMPONENT Runtime)
|
||||
|
||||
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
|
||||
|
||||
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
|
||||
install(FILES "${bundled_library}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endforeach(bundled_library)
|
||||
|
||||
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||
# from a previous install.
|
||||
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||
" COMPONENT Runtime)
|
||||
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||
|
||||
# Install the AOT library on non-Debug builds only.
|
||||
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endif()
|
|
@ -0,0 +1,88 @@
|
|||
# This file controls Flutter-level build steps. It should not be edited.
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||
|
||||
# Configuration provided via flutter tool.
|
||||
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||
|
||||
# TODO: Move the rest of this into files in ephemeral. See
|
||||
# https://github.com/flutter/flutter/issues/57146.
|
||||
|
||||
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
|
||||
# which isn't available in 3.10.
|
||||
function(list_prepend LIST_NAME PREFIX)
|
||||
set(NEW_LIST "")
|
||||
foreach(element ${${LIST_NAME}})
|
||||
list(APPEND NEW_LIST "${PREFIX}${element}")
|
||||
endforeach(element)
|
||||
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# === Flutter Library ===
|
||||
# System-level dependencies.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
|
||||
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
|
||||
|
||||
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
|
||||
|
||||
# Published to parent scope for install step.
|
||||
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
|
||||
|
||||
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||
"fl_basic_message_channel.h"
|
||||
"fl_binary_codec.h"
|
||||
"fl_binary_messenger.h"
|
||||
"fl_dart_project.h"
|
||||
"fl_engine.h"
|
||||
"fl_json_message_codec.h"
|
||||
"fl_json_method_codec.h"
|
||||
"fl_message_codec.h"
|
||||
"fl_method_call.h"
|
||||
"fl_method_channel.h"
|
||||
"fl_method_codec.h"
|
||||
"fl_method_response.h"
|
||||
"fl_plugin_registrar.h"
|
||||
"fl_plugin_registry.h"
|
||||
"fl_standard_message_codec.h"
|
||||
"fl_standard_method_codec.h"
|
||||
"fl_string_codec.h"
|
||||
"fl_value.h"
|
||||
"fl_view.h"
|
||||
"flutter_linux.h"
|
||||
)
|
||||
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
|
||||
add_library(flutter INTERFACE)
|
||||
target_include_directories(flutter INTERFACE
|
||||
"${EPHEMERAL_DIR}"
|
||||
)
|
||||
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
|
||||
target_link_libraries(flutter INTERFACE
|
||||
PkgConfig::GTK
|
||||
PkgConfig::GLIB
|
||||
PkgConfig::GIO
|
||||
)
|
||||
add_dependencies(flutter flutter_assemble)
|
||||
|
||||
# === Flutter tool backend ===
|
||||
# _phony_ is a non-existent file to force this command to run every time,
|
||||
# since currently there's no way to get a full input/output list from the
|
||||
# flutter tool.
|
||||
add_custom_command(
|
||||
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/_phony_
|
||||
COMMAND ${CMAKE_COMMAND} -E env
|
||||
${FLUTTER_TOOL_ENVIRONMENT}
|
||||
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
|
||||
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(flutter_assemble DEPENDS
|
||||
"${FLUTTER_LIBRARY}"
|
||||
${FLUTTER_LIBRARY_HEADERS}
|
||||
)
|
|
@ -0,0 +1,11 @@
|
|||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||
#define GENERATED_PLUGIN_REGISTRANT_
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
|
||||
// Registers Flutter plugins.
|
||||
void fl_register_plugins(FlPluginRegistry* registry);
|
||||
|
||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
|
@ -0,0 +1,23 @@
|
|||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||
endforeach(plugin)
|
||||
|
||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||
endforeach(ffi_plugin)
|
|
@ -0,0 +1,6 @@
|
|||
#include "my_application.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
g_autoptr(MyApplication) app = my_application_new();
|
||||
return g_application_run(G_APPLICATION(app), argc, argv);
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
#include "my_application.h"
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
#endif
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
struct _MyApplication {
|
||||
GtkApplication parent_instance;
|
||||
char** dart_entrypoint_arguments;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
GtkWindow* window =
|
||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||
|
||||
// Use a header bar when running in GNOME as this is the common style used
|
||||
// by applications and is the setup most users will be using (e.g. Ubuntu
|
||||
// desktop).
|
||||
// If running on X and not using GNOME then just use a traditional title bar
|
||||
// in case the window manager does more exotic layout, e.g. tiling.
|
||||
// If running on Wayland assume the header bar will work (may need changing
|
||||
// if future cases occur).
|
||||
gboolean use_header_bar = TRUE;
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
GdkScreen* screen = gtk_window_get_screen(window);
|
||||
if (GDK_IS_X11_SCREEN(screen)) {
|
||||
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
||||
use_header_bar = FALSE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (use_header_bar) {
|
||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||
gtk_header_bar_set_title(header_bar, "subwrite");
|
||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||
} else {
|
||||
gtk_window_set_title(window, "subwrite");
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
gtk_widget_show(GTK_WIDGET(window));
|
||||
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
|
||||
|
||||
FlView* view = fl_view_new(project);
|
||||
gtk_widget_show(GTK_WIDGET(view));
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||
|
||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||
}
|
||||
|
||||
// Implements GApplication::local_command_line.
|
||||
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
// Strip out the first argument as it is the binary name.
|
||||
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
||||
|
||||
g_autoptr(GError) error = nullptr;
|
||||
if (!g_application_register(application, nullptr, &error)) {
|
||||
g_warning("Failed to register: %s", error->message);
|
||||
*exit_status = 1;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
g_application_activate(application);
|
||||
*exit_status = 0;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Implements GObject::dispose.
|
||||
static void my_application_dispose(GObject* object) {
|
||||
MyApplication* self = MY_APPLICATION(object);
|
||||
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
static void my_application_class_init(MyApplicationClass* klass) {
|
||||
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
|
||||
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
||||
}
|
||||
|
||||
static void my_application_init(MyApplication* self) {}
|
||||
|
||||
MyApplication* my_application_new() {
|
||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||
"application-id", APPLICATION_ID,
|
||||
"flags", G_APPLICATION_NON_UNIQUE,
|
||||
nullptr));
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
#ifndef FLUTTER_MY_APPLICATION_H_
|
||||
#define FLUTTER_MY_APPLICATION_H_
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
|
||||
GtkApplication)
|
||||
|
||||
/**
|
||||
* my_application_new:
|
||||
*
|
||||
* Creates a new Flutter-based application.
|
||||
*
|
||||
* Returns: a new #MyApplication.
|
||||
*/
|
||||
MyApplication* my_application_new();
|
||||
|
||||
#endif // FLUTTER_MY_APPLICATION_H_
|
|
@ -0,0 +1,175 @@
|
|||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.9.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.12"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.5"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.2"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: provider
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.12"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
sdks:
|
||||
dart: ">=2.18.0 <3.0.0"
|
||||
flutter: ">=1.16.0"
|
|
@ -0,0 +1,98 @@
|
|||
name: subwrite
|
||||
description: A Ridiculously Personalized Markdown Editor
|
||||
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
|
||||
# The following defines the version and build number for your application.
|
||||
# A version number is three numbers separated by dots, like 1.2.43
|
||||
# followed by an optional build number separated by a +.
|
||||
# Both the version and the builder number may be overridden in flutter
|
||||
# build by specifying --build-name and --build-number, respectively.
|
||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: '>=2.18.0 <3.0.0'
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||
# dependencies can be manually updated by changing the version numbers below to
|
||||
# the latest version available on pub.dev. To see which dependencies have newer
|
||||
# versions available, run `flutter pub outdated`.
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.2
|
||||
provider: ^6.0.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^2.0.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
|
||||
fonts:
|
||||
- family: Iosevka
|
||||
fonts:
|
||||
- asset: fonts/iosevka-light.ttf
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/assets-and-images/#from-packages
|
||||
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
|
@ -0,0 +1,30 @@
|
|||
// This is a basic Flutter widget test.
|
||||
//
|
||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:subwrite/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
expect(find.text('1'), findsNothing);
|
||||
|
||||
// Tap the '+' icon and trigger a frame.
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pump();
|
||||
|
||||
// Verify that our counter has incremented.
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue