Merge branch 'master' into fix2

pull/142/head
Xun Gong 4 years ago
commit fe6ab738c0
  1. 2
      .github/ISSUE_TEMPLATE/issue-template.md
  2. 3
      CHANGELOG.md
  3. 22
      analysis_options.yaml
  4. 34
      example/lib/pages/home_page.dart
  5. 4
      example/lib/pages/read_only_page.dart
  6. 6
      example/lib/universal_ui/universal_ui.dart
  7. 14
      example/lib/widgets/demo_scaffold.dart
  8. 2
      example/test/widget_test.dart
  9. 10
      lib/models/documents/attribute.dart
  10. 82
      lib/models/documents/document.dart
  11. 35
      lib/models/documents/history.dart
  12. 37
      lib/models/documents/nodes/block.dart
  13. 70
      lib/models/documents/nodes/container.dart
  14. 24
      lib/models/documents/nodes/embed.dart
  15. 156
      lib/models/documents/nodes/leaf.dart
  16. 207
      lib/models/documents/nodes/line.dart
  17. 86
      lib/models/documents/nodes/node.dart
  18. 29
      lib/models/documents/style.dart
  19. 86
      lib/models/quill_delta.dart
  20. 39
      lib/models/rules/delete.dart
  21. 44
      lib/models/rules/format.dart
  22. 109
      lib/models/rules/insert.dart
  23. 13
      lib/models/rules/rule.dart
  24. 6
      lib/utils/color.dart
  25. 36
      lib/utils/diff_delta.dart
  26. 3
      lib/widgets/box.dart
  27. 39
      lib/widgets/controller.dart
  28. 68
      lib/widgets/cursor.dart
  29. 117
      lib/widgets/default_styles.dart
  30. 10
      lib/widgets/delegate.dart
  31. 349
      lib/widgets/editor.dart
  32. 11
      lib/widgets/keyboard_listener.dart
  33. 57
      lib/widgets/proxy.dart
  34. 268
      lib/widgets/raw_editor.dart
  35. 8
      lib/widgets/responsive_widget.dart
  36. 246
      lib/widgets/text_block.dart
  37. 234
      lib/widgets/text_line.dart
  38. 113
      lib/widgets/text_selection.dart
  39. 184
      lib/widgets/toolbar.dart
  40. 2
      pubspec.yaml

@ -11,6 +11,6 @@ My issue is about [Web]
My issue is about [Mobile] My issue is about [Mobile]
My issue is about [Desktop] My issue is about [Desktop]
I have tried running `example` directory successfully before creating an issue here AND it is NOT a stupid question. I have tried running `example` directory successfully before creating an issue here.
Please note that we are using stable channel. If you are using beta or master channel, those are not supported. Please note that we are using stable channel. If you are using beta or master channel, those are not supported.

@ -1,3 +1,6 @@
## [1.1.8]
* Fix height of empty line bug.
## [1.1.7] ## [1.1.7]
* Fix text selection in read-only mode. * Fix text selection in read-only mode.

@ -3,13 +3,33 @@ include: package:pedantic/analysis_options.yaml
analyzer: analyzer:
errors: errors:
undefined_prefixed_name: ignore undefined_prefixed_name: ignore
omit_local_variable_types: ignore
unsafe_html: ignore unsafe_html: ignore
linter: linter:
rules: rules:
- always_declare_return_types
- always_put_required_named_parameters_first - always_put_required_named_parameters_first
- annotate_overrides
- avoid_empty_else
- avoid_escaping_inner_quotes
- avoid_print - avoid_print
- avoid_redundant_argument_values - avoid_redundant_argument_values
- avoid_types_on_closure_parameters
- avoid_void_async
- cascade_invocations
- directives_ordering
- omit_local_variable_types
- prefer_const_constructors - prefer_const_constructors
- prefer_const_constructors_in_immutables - prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals
- prefer_initializing_formals
- prefer_int_literals
- prefer_interpolation_to_compose_strings
- prefer_relative_imports
- prefer_single_quotes
- sort_constructors_first
- sort_unnamed_constructors_first
- unnecessary_parenthesis - unnecessary_parenthesis
- unnecessary_string_interpolations

@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:app/universal_ui/universal_ui.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -16,6 +15,7 @@ import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import '../universal_ui/universal_ui.dart';
import 'read_only_page.dart'; import 'read_only_page.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
@ -74,7 +74,7 @@ class _HomePageState extends State<HomePage> {
), ),
body: RawKeyboardListener( body: RawKeyboardListener(
focusNode: FocusNode(), focusNode: FocusNode(),
onKey: (RawKeyEvent event) { onKey: (event) {
if (event.data.isControlPressed && event.character == 'b') { if (event.data.isControlPressed && event.character == 'b') {
if (_controller! if (_controller!
.getSelectionStyle() .getSelectionStyle()
@ -107,15 +107,15 @@ class _HomePageState extends State<HomePage> {
customStyles: DefaultStyles( customStyles: DefaultStyles(
h1: DefaultTextBlockStyle( h1: DefaultTextBlockStyle(
const TextStyle( const TextStyle(
fontSize: 32.0, fontSize: 32,
color: Colors.black, color: Colors.black,
height: 1.15, height: 1.15,
fontWeight: FontWeight.w300, fontWeight: FontWeight.w300,
), ),
const Tuple2(16.0, 0.0), const Tuple2(16, 0),
const Tuple2(0.0, 0.0), const Tuple2(0, 0),
null), null),
sizeSmall: const TextStyle(fontSize: 9.0), sizeSmall: const TextStyle(fontSize: 9),
)); ));
if (kIsWeb) { if (kIsWeb) {
quillEditor = QuillEditor( quillEditor = QuillEditor(
@ -131,15 +131,15 @@ class _HomePageState extends State<HomePage> {
customStyles: DefaultStyles( customStyles: DefaultStyles(
h1: DefaultTextBlockStyle( h1: DefaultTextBlockStyle(
const TextStyle( const TextStyle(
fontSize: 32.0, fontSize: 32,
color: Colors.black, color: Colors.black,
height: 1.15, height: 1.15,
fontWeight: FontWeight.w300, fontWeight: FontWeight.w300,
), ),
const Tuple2(16.0, 0.0), const Tuple2(16, 0),
const Tuple2(0.0, 0.0), const Tuple2(0, 0),
null), null),
sizeSmall: const TextStyle(fontSize: 9.0), sizeSmall: const TextStyle(fontSize: 9),
), ),
embedBuilder: defaultEmbedBuilderWeb); embedBuilder: defaultEmbedBuilderWeb);
} }
@ -151,7 +151,7 @@ class _HomePageState extends State<HomePage> {
flex: 15, flex: 15,
child: Container( child: Container(
color: Colors.white, color: Colors.white,
padding: const EdgeInsets.only(left: 16.0, right: 16.0), padding: const EdgeInsets.only(left: 16, right: 16),
child: quillEditor, child: quillEditor,
), ),
), ),
@ -178,15 +178,15 @@ class _HomePageState extends State<HomePage> {
// You can also upload the picked image to any server (eg : AWS s3 or Firebase) and then return the uploaded image URL // You can also upload the picked image to any server (eg : AWS s3 or Firebase) and then return the uploaded image URL
Future<String> _onImagePickCallback(File file) async { Future<String> _onImagePickCallback(File file) async {
// Copies the picked file from temporary cache to applications directory // Copies the picked file from temporary cache to applications directory
Directory appDocDir = await getApplicationDocumentsDirectory(); final appDocDir = await getApplicationDocumentsDirectory();
File copiedFile = final copiedFile =
await file.copy('${appDocDir.path}/${basename(file.path)}'); await file.copy('${appDocDir.path}/${basename(file.path)}');
return copiedFile.path.toString(); return copiedFile.path.toString();
} }
Widget _buildMenuBar(BuildContext context) { Widget _buildMenuBar(BuildContext context) {
Size size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
final itemStyle = const TextStyle( const itemStyle = TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -201,7 +201,7 @@ class _HomePageState extends State<HomePage> {
endIndent: size.width * 0.1, endIndent: size.width * 0.1,
), ),
ListTile( ListTile(
title: Center(child: Text('Read only demo', style: itemStyle)), title: const Center(child: Text('Read only demo', style: itemStyle)),
dense: true, dense: true,
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
onTap: _readOnly, onTap: _readOnly,
@ -220,7 +220,7 @@ class _HomePageState extends State<HomePage> {
Navigator.push( Navigator.push(
super.context, super.context,
MaterialPageRoute( MaterialPageRoute(
builder: (BuildContext context) => ReadOnlyPage(), builder: (context) => ReadOnlyPage(),
), ),
); );
} }

@ -1,9 +1,9 @@
import 'package:app/universal_ui/universal_ui.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_quill/widgets/controller.dart'; import 'package:flutter_quill/widgets/controller.dart';
import 'package:flutter_quill/widgets/editor.dart'; import 'package:flutter_quill/widgets/editor.dart';
import '../universal_ui/universal_ui.dart';
import '../widgets/demo_scaffold.dart'; import '../widgets/demo_scaffold.dart';
class ReadOnlyPage extends StatefulWidget { class ReadOnlyPage extends StatefulWidget {
@ -53,7 +53,7 @@ class _ReadOnlyPageState extends State<ReadOnlyPage> {
embedBuilder: defaultEmbedBuilderWeb); embedBuilder: defaultEmbedBuilderWeb);
} }
return Padding( return Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,

@ -28,10 +28,10 @@ var ui = UniversalUI();
Widget defaultEmbedBuilderWeb(BuildContext context, leaf.Embed node) { Widget defaultEmbedBuilderWeb(BuildContext context, leaf.Embed node) {
switch (node.value.type) { switch (node.value.type) {
case 'image': case 'image':
String imageUrl = node.value.data; final String imageUrl = node.value.data;
Size size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
UniversalUI().platformViewRegistry.registerViewFactory( UniversalUI().platformViewRegistry.registerViewFactory(
imageUrl, (int viewId) => html.ImageElement()..src = imageUrl); imageUrl, (viewId) => html.ImageElement()..src = imageUrl);
return Padding( return Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
right: ResponsiveWidget.isMediumScreen(context) right: ResponsiveWidget.isMediumScreen(context)

@ -11,13 +11,6 @@ typedef DemoContentBuilder = Widget Function(
// Common scaffold for all examples. // Common scaffold for all examples.
class DemoScaffold extends StatefulWidget { class DemoScaffold extends StatefulWidget {
/// Filename of the document to load into the editor.
final String documentFilename;
final DemoContentBuilder builder;
final List<Widget>? actions;
final Widget? floatingActionButton;
final bool showToolbar;
const DemoScaffold({ const DemoScaffold({
required this.documentFilename, required this.documentFilename,
required this.builder, required this.builder,
@ -27,6 +20,13 @@ class DemoScaffold extends StatefulWidget {
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
/// Filename of the document to load into the editor.
final String documentFilename;
final DemoContentBuilder builder;
final List<Widget>? actions;
final Widget? floatingActionButton;
final bool showToolbar;
@override @override
_DemoScaffoldState createState() => _DemoScaffoldState(); _DemoScaffoldState createState() => _DemoScaffoldState();
} }

@ -10,7 +10,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:app/main.dart'; import 'package:app/main.dart';
void main() { void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async { testWidgets('Counter increments smoke test', (tester) async {
// Build our app and trigger a frame. // Build our app and trigger a frame.
await tester.pumpWidget(MyApp()); await tester.pumpWidget(MyApp());

@ -8,12 +8,12 @@ enum AttributeScope {
} }
class Attribute<T> { class Attribute<T> {
Attribute(this.key, this.scope, this.value);
final String key; final String key;
final AttributeScope scope; final AttributeScope scope;
final T value; final T value;
Attribute(this.key, this.scope, this.value);
static final Map<String, Attribute> _registry = { static final Map<String, Attribute> _registry = {
Attribute.bold.key: Attribute.bold, Attribute.bold.key: Attribute.bold,
Attribute.italic.key: Attribute.italic, Attribute.italic.key: Attribute.italic,
@ -164,8 +164,8 @@ class Attribute<T> {
if (!_registry.containsKey(key)) { if (!_registry.containsKey(key)) {
throw ArgumentError.value(key, 'key "$key" not found.'); throw ArgumentError.value(key, 'key "$key" not found.');
} }
Attribute origin = _registry[key]!; final origin = _registry[key]!;
Attribute attribute = clone(origin, value); final attribute = clone(origin, value);
return attribute; return attribute;
} }
@ -177,7 +177,7 @@ class Attribute<T> {
bool operator ==(Object other) { bool operator ==(Object other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
if (other is! Attribute<T>) return false; if (other is! Attribute<T>) return false;
Attribute<T> typedOther = other; final typedOther = other;
return key == typedOther.key && return key == typedOther.key &&
scope == typedOther.scope && scope == typedOther.scope &&
value == typedOther.value; value == typedOther.value;

@ -1,20 +1,32 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter_quill/models/documents/nodes/block.dart';
import 'package:flutter_quill/models/documents/nodes/container.dart';
import 'package:flutter_quill/models/documents/nodes/line.dart';
import 'package:flutter_quill/models/documents/style.dart';
import 'package:flutter_quill/models/quill_delta.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import '../quill_delta.dart';
import '../rules/rule.dart'; import '../rules/rule.dart';
import 'attribute.dart'; import 'attribute.dart';
import 'history.dart'; import 'history.dart';
import 'nodes/block.dart';
import 'nodes/container.dart';
import 'nodes/embed.dart'; import 'nodes/embed.dart';
import 'nodes/line.dart';
import 'nodes/node.dart'; import 'nodes/node.dart';
import 'style.dart';
/// The rich text document /// The rich text document
class Document { class Document {
Document() : _delta = Delta()..insert('\n') {
_loadDocument(_delta);
}
Document.fromJson(List data) : _delta = _transform(Delta.fromJson(data)) {
_loadDocument(_delta);
}
Document.fromDelta(Delta delta) : _delta = delta {
_loadDocument(delta);
}
/// The root node of the document tree /// The root node of the document tree
final Root _root = Root(); final Root _root = Root();
@ -35,18 +47,6 @@ class Document {
Stream<Tuple3<Delta, Delta, ChangeSource>> get changes => _observer.stream; Stream<Tuple3<Delta, Delta, ChangeSource>> get changes => _observer.stream;
Document() : _delta = Delta()..insert('\n') {
_loadDocument(_delta);
}
Document.fromJson(List data) : _delta = _transform(Delta.fromJson(data)) {
_loadDocument(_delta);
}
Document.fromDelta(Delta delta) : _delta = delta {
_loadDocument(delta);
}
Delta insert(int index, Object? data) { Delta insert(int index, Object? data) {
assert(index >= 0); assert(index >= 0);
assert(data is String || data is Embeddable); assert(data is String || data is Embeddable);
@ -56,14 +56,14 @@ class Document {
return Delta(); return Delta();
} }
Delta delta = _rules.apply(RuleType.INSERT, this, index, data: data); final delta = _rules.apply(RuleType.INSERT, this, index, data: data);
compose(delta, ChangeSource.LOCAL); compose(delta, ChangeSource.LOCAL);
return delta; return delta;
} }
Delta delete(int index, int len) { Delta delete(int index, int len) {
assert(index >= 0 && len > 0); assert(index >= 0 && len > 0);
Delta delta = _rules.apply(RuleType.DELETE, this, index, len: len); final delta = _rules.apply(RuleType.DELETE, this, index, len: len);
if (delta.isNotEmpty) { if (delta.isNotEmpty) {
compose(delta, ChangeSource.LOCAL); compose(delta, ChangeSource.LOCAL);
} }
@ -74,18 +74,18 @@ class Document {
assert(index >= 0); assert(index >= 0);
assert(data is String || data is Embeddable); assert(data is String || data is Embeddable);
bool dataIsNotEmpty = (data is String) ? data.isNotEmpty : true; final dataIsNotEmpty = (data is String) ? data.isNotEmpty : true;
assert(dataIsNotEmpty || len > 0); assert(dataIsNotEmpty || len > 0);
Delta delta = Delta(); var delta = Delta();
if (dataIsNotEmpty) { if (dataIsNotEmpty) {
delta = insert(index + len, data); delta = insert(index + len, data);
} }
if (len > 0) { if (len > 0) {
Delta deleteDelta = delete(index, len); final deleteDelta = delete(index, len);
delta = delta.compose(deleteDelta); delta = delta.compose(deleteDelta);
} }
@ -95,9 +95,9 @@ class Document {
Delta format(int index, int len, Attribute? attribute) { Delta format(int index, int len, Attribute? attribute) {
assert(index >= 0 && len >= 0 && attribute != null); assert(index >= 0 && len >= 0 && attribute != null);
Delta delta = Delta(); var delta = Delta();
Delta formatDelta = _rules.apply(RuleType.FORMAT, this, index, final formatDelta = _rules.apply(RuleType.FORMAT, this, index,
len: len, attribute: attribute); len: len, attribute: attribute);
if (formatDelta.isNotEmpty) { if (formatDelta.isNotEmpty) {
compose(formatDelta, ChangeSource.LOCAL); compose(formatDelta, ChangeSource.LOCAL);
@ -108,16 +108,16 @@ class Document {
} }
Style collectStyle(int index, int len) { Style collectStyle(int index, int len) {
ChildQuery res = queryChild(index); final res = queryChild(index);
return (res.node as Line).collectStyle(res.offset, len); return (res.node as Line).collectStyle(res.offset, len);
} }
ChildQuery queryChild(int offset) { ChildQuery queryChild(int offset) {
ChildQuery res = _root.queryChild(offset, true); final res = _root.queryChild(offset, true);
if (res.node is Line) { if (res.node is Line) {
return res; return res;
} }
Block block = res.node as Block; final block = res.node as Block;
return block.queryChild(res.offset, true); return block.queryChild(res.offset, true);
} }
@ -126,11 +126,11 @@ class Document {
delta.trim(); delta.trim();
assert(delta.isNotEmpty); assert(delta.isNotEmpty);
int offset = 0; var offset = 0;
delta = _transform(delta); delta = _transform(delta);
Delta originalDelta = toDelta(); final originalDelta = toDelta();
for (Operation op in delta.toList()) { for (final op in delta.toList()) {
Style? style = final style =
op.attributes != null ? Style.fromJson(op.attributes) : null; op.attributes != null ? Style.fromJson(op.attributes) : null;
if (op.isInsert) { if (op.isInsert) {
@ -172,10 +172,10 @@ class Document {
bool get hasRedo => _history.hasRedo; bool get hasRedo => _history.hasRedo;
static Delta _transform(Delta delta) { static Delta _transform(Delta delta) {
Delta res = Delta(); final res = Delta();
List<Operation> ops = delta.toList(); final ops = delta.toList();
for (int i = 0; i < ops.length; i++) { for (var i = 0; i < ops.length; i++) {
Operation op = ops[i]; final op = ops[i];
res.push(op); res.push(op);
_handleImageInsert(i, ops, op, res); _handleImageInsert(i, ops, op, res);
} }
@ -184,14 +184,14 @@ class Document {
static void _handleImageInsert( static void _handleImageInsert(
int i, List<Operation> ops, Operation op, Delta res) { int i, List<Operation> ops, Operation op, Delta res) {
bool nextOpIsImage = final nextOpIsImage =
i + 1 < ops.length && ops[i + 1].isInsert && ops[i + 1].data is! String; i + 1 < ops.length && ops[i + 1].isInsert && ops[i + 1].data is! String;
if (nextOpIsImage && !(op.data as String).endsWith('\n')) { if (nextOpIsImage && !(op.data as String).endsWith('\n')) {
res.push(Operation.insert('\n')); res.push(Operation.insert('\n'));
} }
// Currently embed is equivalent to image and hence `is! String` // Currently embed is equivalent to image and hence `is! String`
bool opInsertImage = op.isInsert && op.data is! String; final opInsertImage = op.isInsert && op.data is! String;
bool nextOpIsLineBreak = i + 1 < ops.length && final nextOpIsLineBreak = i + 1 < ops.length &&
ops[i + 1].isInsert && ops[i + 1].isInsert &&
ops[i + 1].data is String && ops[i + 1].data is String &&
(ops[i + 1].data as String).startsWith('\n'); (ops[i + 1].data as String).startsWith('\n');
@ -221,7 +221,7 @@ class Document {
void _loadDocument(Delta doc) { void _loadDocument(Delta doc) {
assert((doc.last.data as String).endsWith('\n')); assert((doc.last.data as String).endsWith('\n'));
int offset = 0; var offset = 0;
for (final op in doc.toList()) { for (final op in doc.toList()) {
if (!op.isInsert) { if (!op.isInsert) {
throw ArgumentError.value(doc, throw ArgumentError.value(doc,
@ -247,12 +247,12 @@ class Document {
return false; return false;
} }
final Node node = root.children.first; final node = root.children.first;
if (!node.isLast) { if (!node.isLast) {
return false; return false;
} }
Delta delta = node.toDelta(); final delta = node.toDelta();
return delta.length == 1 && return delta.length == 1 &&
delta.first.data == '\n' && delta.first.data == '\n' &&
delta.first.key == 'insert'; delta.first.key == 'insert';

@ -1,9 +1,17 @@
import 'package:flutter_quill/models/quill_delta.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import '../quill_delta.dart';
import 'document.dart'; import 'document.dart';
class History { class History {
History({
this.ignoreChange = false,
this.interval = 400,
this.maxStack = 100,
this.userOnly = false,
this.lastRecorded = 0,
});
final HistoryStack stack = HistoryStack.empty(); final HistoryStack stack = HistoryStack.empty();
bool get hasUndo => stack.undo.isNotEmpty; bool get hasUndo => stack.undo.isNotEmpty;
@ -24,13 +32,6 @@ class History {
///record delay ///record delay
final int interval; final int interval;
History(
{this.ignoreChange = false,
this.interval = 400,
this.maxStack = 100,
this.userOnly = false,
this.lastRecorded = 0});
void handleDocChange(Tuple3<Delta, Delta, ChangeSource> change) { void handleDocChange(Tuple3<Delta, Delta, ChangeSource> change) {
if (ignoreChange) return; if (ignoreChange) return;
if (!userOnly || change.item3 == ChangeSource.LOCAL) { if (!userOnly || change.item3 == ChangeSource.LOCAL) {
@ -47,7 +48,7 @@ class History {
void record(Delta change, Delta before) { void record(Delta change, Delta before) {
if (change.isEmpty) return; if (change.isEmpty) return;
stack.redo.clear(); stack.redo.clear();
Delta undoDelta = change.invert(before); var undoDelta = change.invert(before);
final timeStamp = DateTime.now().millisecondsSinceEpoch; final timeStamp = DateTime.now().millisecondsSinceEpoch;
if (lastRecorded + interval > timeStamp && stack.undo.isNotEmpty) { if (lastRecorded + interval > timeStamp && stack.undo.isNotEmpty) {
@ -74,7 +75,7 @@ class History {
} }
void transformStack(List<Delta> stack, Delta delta) { void transformStack(List<Delta> stack, Delta delta) {
for (int i = stack.length - 1; i >= 0; i -= 1) { for (var i = stack.length - 1; i >= 0; i -= 1) {
final oldDelta = stack[i]; final oldDelta = stack[i];
stack[i] = delta.transform(oldDelta, true); stack[i] = delta.transform(oldDelta, true);
delta = oldDelta.transform(delta, false); delta = oldDelta.transform(delta, false);
@ -88,10 +89,10 @@ class History {
if (source.isEmpty) { if (source.isEmpty) {
return const Tuple2(false, 0); return const Tuple2(false, 0);
} }
Delta delta = source.removeLast(); final delta = source.removeLast();
// look for insert or delete // look for insert or delete
int? len = 0; int? len = 0;
List<Operation> ops = delta.toList(); final ops = delta.toList();
for (var i = 0; i < ops.length; i++) { for (var i = 0; i < ops.length; i++) {
if (ops[i].key == Operation.insertKey) { if (ops[i].key == Operation.insertKey) {
len = ops[i].length; len = ops[i].length;
@ -99,8 +100,8 @@ class History {
len = ops[i].length! * -1; len = ops[i].length! * -1;
} }
} }
Delta base = Delta.from(doc.toDelta()); final base = Delta.from(doc.toDelta());
Delta inverseDelta = delta.invert(base); final inverseDelta = delta.invert(base);
dest.add(inverseDelta); dest.add(inverseDelta);
lastRecorded = 0; lastRecorded = 0;
ignoreChange = true; ignoreChange = true;
@ -119,13 +120,13 @@ class History {
} }
class HistoryStack { class HistoryStack {
final List<Delta> undo;
final List<Delta> redo;
HistoryStack.empty() HistoryStack.empty()
: undo = [], : undo = [],
redo = []; redo = [];
final List<Delta> undo;
final List<Delta> redo;
void clear() { void clear() {
undo.clear(); undo.clear();
redo.clear(); redo.clear();

@ -1,10 +1,23 @@
import 'package:flutter_quill/models/quill_delta.dart'; import '../../quill_delta.dart';
import 'container.dart'; import 'container.dart';
import 'line.dart'; import 'line.dart';
import 'node.dart'; import 'node.dart';
/// Represents a group of adjacent [Line]s with the same block style.
///
/// Block elements are:
/// - Blockquote
/// - Header
/// - Indent
/// - List
/// - Text Alignment
/// - Text Direction
/// - Code Block
class Block extends Container<Line?> { class Block extends Container<Line?> {
/// Creates new unmounted [Block].
@override
Node newInstance() => Block();
@override @override
Line get defaultChild => Line(); Line get defaultChild => Line();
@ -18,7 +31,7 @@ class Block extends Container<Line?> {
@override @override
void adjust() { void adjust() {
if (isEmpty) { if (isEmpty) {
Node? sibling = previous; final sibling = previous;
unlink(); unlink();
if (sibling != null) { if (sibling != null) {
sibling.adjust(); sibling.adjust();
@ -26,17 +39,18 @@ class Block extends Container<Line?> {
return; return;
} }
Block block = this; var block = this;
Node? prev = block.previous; final prev = block.previous;
// merging it with previous block if style is the same // merging it with previous block if style is the same
if (!block.isFirst && if (!block.isFirst &&
block.previous is Block && block.previous is Block &&
prev!.style == block.style) { prev!.style == block.style) {
block.moveChildToNewParent(prev as Container<Node?>?); block
block.unlink(); ..moveChildToNewParent(prev as Container<Node?>?)
..unlink();
block = prev as Block; block = prev as Block;
} }
Node? next = block.next; final next = block.next;
// merging it with next block if style is the same // merging it with next block if style is the same
if (!block.isLast && block.next is Block && next!.style == block.style) { if (!block.isLast && block.next is Block && next!.style == block.style) {
(next as Block).moveChildToNewParent(block); (next as Block).moveChildToNewParent(block);
@ -48,16 +62,11 @@ class Block extends Container<Line?> {
String toString() { String toString() {
final block = style.attributes.toString(); final block = style.attributes.toString();
final buffer = StringBuffer('§ {$block}\n'); final buffer = StringBuffer('§ {$block}\n');
for (var child in children) { for (final child in children) {
final tree = child.isLast ? '' : ''; final tree = child.isLast ? '' : '';
buffer.write(' $tree $child'); buffer.write(' $tree $child');
if (!child.isLast) buffer.writeln(); if (!child.isLast) buffer.writeln();
} }
return buffer.toString(); return buffer.toString();
} }
@override
Node newInstance() {
return Block();
}
} }

@ -1,70 +1,99 @@
import 'dart:collection'; import 'dart:collection';
import '../style.dart'; import '../style.dart';
import 'leaf.dart';
import 'line.dart';
import 'node.dart'; import 'node.dart';
/* Container of multiple nodes */ /// Container can accommodate other nodes.
///
/// Delegates insert, retain and delete operations to children nodes. For each
/// operation container looks for a child at specified index position and
/// forwards operation to that child.
///
/// Most of the operation handling logic is implemented by [Line] and [Text].
abstract class Container<T extends Node?> extends Node { abstract class Container<T extends Node?> extends Node {
final LinkedList<Node> _children = LinkedList<Node>(); final LinkedList<Node> _children = LinkedList<Node>();
/// List of children.
LinkedList<Node> get children => _children; LinkedList<Node> get children => _children;
/// Returns total number of child nodes in this container.
///
/// To get text length of this container see [length].
int get childCount => _children.length; int get childCount => _children.length;
/// Returns the first child [Node].
Node get first => _children.first; Node get first => _children.first;
/// Returns the last child [Node].
Node get last => _children.last; Node get last => _children.last;
/// Returns `true` if this container has no child nodes.
bool get isEmpty => _children.isEmpty; bool get isEmpty => _children.isEmpty;
/// Returns `true` if this container has at least 1 child.
bool get isNotEmpty => _children.isNotEmpty; bool get isNotEmpty => _children.isNotEmpty;
/// abstract methods begin /// Returns an instance of default child for this container node.
///
/// Always returns fresh instance.
T get defaultChild; T get defaultChild;
/// abstract methods end /// Adds [node] to the end of this container children list.
void add(T node) { void add(T node) {
assert(node?.parent == null); assert(node?.parent == null);
node?.parent = this; node?.parent = this;
_children.add(node as Node); _children.add(node as Node);
} }
/// Adds [node] to the beginning of this container children list.
void addFirst(T node) { void addFirst(T node) {
assert(node?.parent == null); assert(node?.parent == null);
node?.parent = this; node?.parent = this;
_children.addFirst(node as Node); _children.addFirst(node as Node);
} }
/// Removes [node] from this container.
void remove(T node) { void remove(T node) {
assert(node?.parent == this); assert(node?.parent == this);
node?.parent = null; node?.parent = null;
_children.remove(node as Node); _children.remove(node as Node);
} }
/// Moves children of this node to [newParent].
void moveChildToNewParent(Container? newParent) { void moveChildToNewParent(Container? newParent) {
if (isEmpty) { if (isEmpty) {
return; return;
} }
T? last = newParent!.isEmpty ? null : newParent.last as T?; final last = newParent!.isEmpty ? null : newParent.last as T?;
while (isNotEmpty) { while (isNotEmpty) {
T child = first as T; final child = first as T;
child?.unlink(); child?.unlink();
newParent.add(child); newParent.add(child);
} }
/// In case [newParent] already had children we need to make sure
/// combined list is optimized.
if (last != null) last.adjust(); if (last != null) last.adjust();
} }
/// Queries the child [Node] at specified character [offset] in this container.
///
/// The result may contain the found node or `null` if no node is found
/// at specified offset.
///
/// [ChildQuery.offset] is set to relative offset within returned child node
/// which points at the same character position in the document as the
/// original [offset].
ChildQuery queryChild(int offset, bool inclusive) { ChildQuery queryChild(int offset, bool inclusive) {
if (offset < 0 || offset > length) { if (offset < 0 || offset > length) {
return ChildQuery(null, 0); return ChildQuery(null, 0);
} }
for (Node node in children) { for (final node in children) {
int len = node.length; final len = node.length;
if (offset < len || (inclusive && offset == len && (node.isLast))) { if (offset < len || (inclusive && offset == len && (node.isLast))) {
return ChildQuery(node, offset); return ChildQuery(node, offset);
} }
@ -76,6 +105,9 @@ abstract class Container<T extends Node?> extends Node {
@override @override
String toPlainText() => children.map((child) => child.toPlainText()).join(); String toPlainText() => children.map((child) => child.toPlainText()).join();
/// Content length of this node's children.
///
/// To get number of children in this node use [childCount].
@override @override
int get length => _children.fold(0, (cur, node) => cur + node.length); int get length => _children.fold(0, (cur, node) => cur + node.length);
@ -84,14 +116,14 @@ abstract class Container<T extends Node?> extends Node {
assert(index == 0 || (index > 0 && index < length)); assert(index == 0 || (index > 0 && index < length));
if (isNotEmpty) { if (isNotEmpty) {
ChildQuery child = queryChild(index, false); final child = queryChild(index, false);
child.node!.insert(child.offset, data, style); child.node!.insert(child.offset, data, style);
return; return;
} }
// empty // empty
assert(index == 0); assert(index == 0);
T node = defaultChild; final node = defaultChild;
add(node); add(node);
node?.insert(index, data, style); node?.insert(index, data, style);
} }
@ -99,14 +131,14 @@ abstract class Container<T extends Node?> extends Node {
@override @override
void retain(int index, int? length, Style? attributes) { void retain(int index, int? length, Style? attributes) {
assert(isNotEmpty); assert(isNotEmpty);
ChildQuery child = queryChild(index, false); final child = queryChild(index, false);
child.node!.retain(child.offset, length, attributes); child.node!.retain(child.offset, length, attributes);
} }
@override @override
void delete(int index, int? length) { void delete(int index, int? length) {
assert(isNotEmpty); assert(isNotEmpty);
ChildQuery child = queryChild(index, false); final child = queryChild(index, false);
child.node!.delete(child.offset, length); child.node!.delete(child.offset, length);
} }
@ -114,11 +146,15 @@ abstract class Container<T extends Node?> extends Node {
String toString() => _children.join('\n'); String toString() => _children.join('\n');
} }
/// Query of a child in a Container /// Result of a child query in a [Container].
class ChildQuery { class ChildQuery {
final Node? node; // null if not found ChildQuery(this.node, this.offset);
final int offset; /// The child node if found, otherwise `null`.
final Node? node;
ChildQuery(this.node, this.offset); /// Starting offset within the child [node] which points at the same
/// character in the document as the original offset passed to
/// [Container.queryChild] method.
final int offset;
} }

@ -1,26 +1,40 @@
/// An object which can be embedded into a Quill document.
///
/// See also:
///
/// * [BlockEmbed] which represents a block embed.
class Embeddable { class Embeddable {
Embeddable(this.type, this.data);
/// The type of this object.
final String type; final String type;
final dynamic data;
Embeddable(this.type, this.data); /// The data payload of this object.
final dynamic data;
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
Map<String, String> m = {type: data}; final m = <String, String>{type: data};
return m; return m;
} }
static Embeddable fromJson(Map<String, dynamic> json) { static Embeddable fromJson(Map<String, dynamic> json) {
Map<String, dynamic> m = Map<String, dynamic>.from(json); final m = Map<String, dynamic>.from(json);
assert(m.length == 1, 'Embeddable map has one key'); assert(m.length == 1, 'Embeddable map has one key');
return BlockEmbed(m.keys.first, m.values.first); return BlockEmbed(m.keys.first, m.values.first);
} }
} }
/// An object which occupies an entire line in a document and cannot co-exist
/// inline with regular text.
///
/// There are two built-in embed types supported by Quill documents, however
/// the document model itself does not make any assumptions about the types
/// of embedded objects and allows users to define their own types.
class BlockEmbed extends Embeddable { class BlockEmbed extends Embeddable {
BlockEmbed(String type, String data) : super(type, data); BlockEmbed(String type, String data) : super(type, data);
static final BlockEmbed horizontalRule = BlockEmbed('divider', 'hr'); static BlockEmbed horizontalRule = BlockEmbed('divider', 'hr');
static BlockEmbed image(String imageUrl) => BlockEmbed('image', imageUrl); static BlockEmbed image(String imageUrl) => BlockEmbed('image', imageUrl);
} }

@ -1,29 +1,30 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter_quill/models/quill_delta.dart'; import '../../quill_delta.dart';
import '../style.dart'; import '../style.dart';
import 'embed.dart'; import 'embed.dart';
import 'line.dart'; import 'line.dart';
import 'node.dart'; import 'node.dart';
/* A leaf node in document tree */ /// A leaf in Quill document tree.
abstract class Leaf extends Node { abstract class Leaf extends Node {
Object _value; /// Creates a new [Leaf] with specified [data].
Object get value => _value;
Leaf.val(Object val) : _value = val;
factory Leaf(Object data) { factory Leaf(Object data) {
if (data is Embeddable) { if (data is Embeddable) {
return Embed(data); return Embed(data);
} }
String text = data as String; final text = data as String;
assert(text.isNotEmpty); assert(text.isNotEmpty);
return Text(text); return Text(text);
} }
Leaf.val(Object val) : _value = val;
/// Contents of this node, either a String if this is a [Text] or an
/// [Embed] if this is an [BlockEmbed].
Object get value => _value;
Object _value;
@override @override
void applyStyle(Style value) { void applyStyle(Style value) {
assert(value.isInline || value.isIgnored || value.isEmpty, assert(value.isInline || value.isIgnored || value.isEmpty,
@ -45,14 +46,15 @@ abstract class Leaf extends Node {
@override @override
Delta toDelta() { Delta toDelta() {
var data = _value is Embeddable ? (_value as Embeddable).toJson() : _value; final data =
_value is Embeddable ? (_value as Embeddable).toJson() : _value;
return Delta()..insert(data, style.toJson()); return Delta()..insert(data, style.toJson());
} }
@override @override
void insert(int index, Object data, Style? style) { void insert(int index, Object data, Style? style) {
assert(index >= 0 && index <= length); assert(index >= 0 && index <= length);
Leaf node = Leaf(data); final node = Leaf(data);
if (index < length) { if (index < length) {
splitAt(index)!.insertBefore(node); splitAt(index)!.insertBefore(node);
} else { } else {
@ -67,9 +69,9 @@ abstract class Leaf extends Node {
return; return;
} }
int local = math.min(length - index, len!); final local = math.min(length - index, len!);
int remain = len - local; final remain = len - local;
Leaf node = _isolate(index, local); final node = _isolate(index, local);
if (remain > 0) { if (remain > 0) {
assert(node.next != null); assert(node.next != null);
@ -82,13 +84,13 @@ abstract class Leaf extends Node {
void delete(int index, int? len) { void delete(int index, int? len) {
assert(index < length); assert(index < length);
int local = math.min(length - index, len!); final local = math.min(length - index, len!);
Leaf target = _isolate(index, local); final target = _isolate(index, local);
Leaf? prev = target.previous as Leaf?; final prev = target.previous as Leaf?;
Leaf? next = target.next as Leaf?; final next = target.next as Leaf?;
target.unlink(); target.unlink();
int remain = len - local; final remain = len - local;
if (remain > 0) { if (remain > 0) {
assert(next != null); assert(next != null);
next!.delete(0, remain); next!.delete(0, remain);
@ -99,36 +101,47 @@ abstract class Leaf extends Node {
} }
} }
/// Adjust this text node by merging it with adjacent nodes if they share
/// the same style.
@override @override
void adjust() { void adjust() {
if (this is Embed) { if (this is Embed) {
// Embed nodes cannot be merged with text nor other embeds (in fact,
// there could be no two adjacent embeds on the same line since an
// embed occupies an entire line).
return; return;
} }
Text node = this as Text; // This is a text node and it can only be merged with other text nodes.
// merging it with previous node if style is the same var node = this as Text;
Node? prev = node.previous;
// Merging it with previous node if style is the same.
final prev = node.previous;
if (!node.isFirst && prev is Text && prev.style == node.style) { if (!node.isFirst && prev is Text && prev.style == node.style) {
prev._value = prev.value + node.value; prev._value = prev.value + node.value;
node.unlink(); node.unlink();
node = prev; node = prev;
} }
// merging it with next node if style is the same // Merging it with next node if style is the same.
Node? next = node.next; final next = node.next;
if (!node.isLast && next is Text && next.style == node.style) { if (!node.isLast && next is Text && next.style == node.style) {
node._value = node.value + next.value; node._value = node.value + next.value;
next.unlink(); next.unlink();
} }
} }
Leaf? cutAt(int index) { /// Splits this leaf node at [index] and returns new node.
assert(index >= 0 && index <= length); ///
Leaf? cut = splitAt(index); /// If this is the last node in its list and [index] equals this node's
cut?.unlink(); /// length then this method returns `null` as there is nothing left to split.
return cut; /// If there is another leaf node after this one and [index] equals this
} /// node's length then the next leaf node is returned.
///
/// If [index] equals to `0` then this node itself is returned unchanged.
///
/// In case a new node is actually split from this one, it inherits this
/// node's style.
Leaf? splitAt(int index) { Leaf? splitAt(int index) {
assert(index >= 0 && index <= length); assert(index >= 0 && index <= length);
if (index == 0) { if (index == 0) {
@ -139,64 +152,101 @@ abstract class Leaf extends Node {
} }
assert(this is Text); assert(this is Text);
String text = _value as String; final text = _value as String;
_value = text.substring(0, index); _value = text.substring(0, index);
Leaf split = Leaf(text.substring(index)); final split = Leaf(text.substring(index))..applyStyle(style);
split.applyStyle(style);
insertAfter(split); insertAfter(split);
return split; return split;
} }
/// Cuts a leaf from [index] to the end of this node and returns new node
/// in detached state (e.g. [mounted] returns `false`).
///
/// Splitting logic is identical to one described in [splitAt], meaning this
/// method may return `null`.
Leaf? cutAt(int index) {
assert(index >= 0 && index <= length);
final cut = splitAt(index);
cut?.unlink();
return cut;
}
/// Formats this node and optimizes it with adjacent leaf nodes if needed.
void format(Style? style) { void format(Style? style) {
if (style != null && style.isNotEmpty) { if (style != null && style.isNotEmpty) {
applyStyle(style); applyStyle(style);
} }
adjust(); adjust();
} }
/// Isolates a new leaf starting at [index] with specified [length].
///
/// Splitting logic is identical to one described in [splitAt], with one
/// exception that it is required for [index] to always be less than this
/// node's length. As a result this method always returns a [LeafNode]
/// instance. Returned node may still be the same as this node
/// if provided [index] is `0`.
Leaf _isolate(int index, int length) { Leaf _isolate(int index, int length) {
assert( assert(
index >= 0 && index < this.length && (index + length <= this.length)); index >= 0 && index < this.length && (index + length <= this.length));
Leaf target = splitAt(index)!; final target = splitAt(index)!..splitAt(length);
target.splitAt(length);
return target; return target;
} }
} }
/// A span of formatted text within a line in a Quill document.
///
/// Text is a leaf node of a document tree.
///
/// Parent of a text node is always a [Line], and as a consequence text
/// node's [value] cannot contain any line-break characters.
///
/// See also:
///
/// * [Embed], a leaf node representing an embeddable object.
/// * [Line], a node representing a line of text.
class Text extends Leaf { class Text extends Leaf {
Text([String text = '']) Text([String text = ''])
: assert(!text.contains('\n')), : assert(!text.contains('\n')),
super.val(text); super.val(text);
@override @override
String get value => _value as String; Node newInstance() => Text();
@override @override
String toPlainText() { String get value => _value as String;
return value;
}
@override @override
Node newInstance() { String toPlainText() => value;
return Text();
}
} }
/// An embedded node such as image or video /// An embed node inside of a line in a Quill document.
///
/// Embed node is a leaf node similar to [Text]. It represents an arbitrary
/// piece of non-textual content embedded into a document, such as, image,
/// horizontal rule, video, or any other object with defined structure,
/// like a tweet, for instance.
///
/// Embed node's length is always `1` character and it is represented with
/// unicode object replacement character in the document text.
///
/// Any inline style can be applied to an embed, however this does not
/// necessarily mean the embed will look according to that style. For instance,
/// applying "bold" style to an image gives no effect, while adding a "link" to
/// an image actually makes the image react to user's action.
class Embed extends Leaf { class Embed extends Leaf {
Embed(Embeddable data) : super.val(data); Embed(Embeddable data) : super.val(data);
static const kObjectReplacementCharacter = '\uFFFC';
@override @override
Embeddable get value => super.value as Embeddable; Node newInstance() => throw UnimplementedError();
@override @override
String toPlainText() { Embeddable get value => super.value as Embeddable;
return '\uFFFC';
}
/// // Embed nodes are represented as unicode object replacement character in
// plain text.
@override @override
Node newInstance() { String toPlainText() => kObjectReplacementCharacter;
throw UnimplementedError();
}
} }

@ -1,15 +1,20 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter_quill/models/documents/attribute.dart'; import '../../quill_delta.dart';
import 'package:flutter_quill/models/documents/nodes/node.dart'; import '../attribute.dart';
import 'package:flutter_quill/models/quill_delta.dart';
import '../style.dart'; import '../style.dart';
import 'block.dart'; import 'block.dart';
import 'container.dart'; import 'container.dart';
import 'embed.dart'; import 'embed.dart';
import 'leaf.dart'; import 'leaf.dart';
import 'node.dart';
/// A line of rich text in a Quill document.
///
/// Line serves as a container for [Leaf]s, like [Text] and [Embed].
///
/// When a line contains an embed, it fully occupies the line, no other embeds
/// or text nodes are allowed.
class Line extends Container<Leaf?> { class Line extends Container<Leaf?> {
@override @override
Leaf get defaultChild => Text(); Leaf get defaultChild => Text();
@ -17,6 +22,7 @@ class Line extends Container<Leaf?> {
@override @override
int get length => super.length + 1; int get length => super.length + 1;
/// Returns `true` if this line contains an embedded object.
bool get hasEmbed { bool get hasEmbed {
if (childCount != 1) { if (childCount != 1) {
return false; return false;
@ -25,6 +31,7 @@ class Line extends Container<Leaf?> {
return children.single is Embed; return children.single is Embed;
} }
/// Returns next [Line] or `null` if this is the last line in the document.
Line? get nextLine { Line? get nextLine {
if (!isLast) { if (!isLast) {
return next is Block ? (next as Block).first as Line? : next as Line?; return next is Block ? (next as Block).first as Line? : next as Line?;
@ -41,6 +48,9 @@ class Line extends Container<Leaf?> {
: parent!.next as Line?; : parent!.next as Line?;
} }
@override
Node newInstance() => Line();
@override @override
Delta toDelta() { Delta toDelta() {
final delta = children final delta = children
@ -48,7 +58,7 @@ class Line extends Container<Leaf?> {
.fold(Delta(), (dynamic a, b) => a.concat(b)); .fold(Delta(), (dynamic a, b) => a.concat(b));
var attributes = style; var attributes = style;
if (parent is Block) { if (parent is Block) {
Block block = parent as Block; final block = parent as Block;
attributes = attributes.mergeAll(block.style); attributes = attributes.mergeAll(block.style);
} }
delta.insert('\n', attributes.toJson()); delta.insert('\n', attributes.toJson());
@ -56,7 +66,7 @@ class Line extends Container<Leaf?> {
} }
@override @override
String toPlainText() => super.toPlainText() + '\n'; String toPlainText() => '${super.toPlainText()}\n';
@override @override
String toString() { String toString() {
@ -68,35 +78,43 @@ class Line extends Container<Leaf?> {
@override @override
void insert(int index, Object data, Style? style) { void insert(int index, Object data, Style? style) {
if (data is Embeddable) { if (data is Embeddable) {
_insert(index, data, style); // We do not check whether this line already has any children here as
// inserting an embed into a line with other text is acceptable from the
// Delta format perspective.
// We rely on heuristic rules to ensure that embeds occupy an entire line.
_insertSafe(index, data, style);
return; return;
} }
String text = data as String; final text = data as String;
int lineBreak = text.indexOf('\n'); final lineBreak = text.indexOf('\n');
if (lineBreak < 0) { if (lineBreak < 0) {
_insert(index, text, style); _insertSafe(index, text, style);
// No need to update line or block format since those attributes can only
// be attached to `\n` character and we already know it's not present.
return; return;
} }
String prefix = text.substring(0, lineBreak); final prefix = text.substring(0, lineBreak);
_insert(index, prefix, style); _insertSafe(index, prefix, style);
if (prefix.isNotEmpty) { if (prefix.isNotEmpty) {
index += prefix.length; index += prefix.length;
} }
Line nextLine = _getNextLine(index); // Next line inherits our format.
final nextLine = _getNextLine(index);
// Reset our format and unwrap from a block if needed.
clearStyle(); clearStyle();
if (parent is Block) { if (parent is Block) {
_unwrap(); _unwrap();
} }
// Now we can apply new format and re-layout.
_format(style); _format(style);
// Continue with the remaining // Continue with remaining part.
String remain = text.substring(lineBreak + 1); final remain = text.substring(lineBreak + 1);
nextLine.insert(0, remain, style); nextLine.insert(0, remain, style);
} }
@ -105,20 +123,24 @@ class Line extends Container<Leaf?> {
if (style == null) { if (style == null) {
return; return;
} }
int thisLen = length; final thisLength = length;
int local = math.min(thisLen - index, len!); final local = math.min(thisLength - index, len!);
// If index is at newline character then this is a line/block style update.
final isLineFormat = (index + local == thisLength) && local == 1;
if (index + local == thisLen && local == 1) { if (isLineFormat) {
assert(style.values.every((attr) => attr.scope == AttributeScope.BLOCK)); assert(style.values.every((attr) => attr.scope == AttributeScope.BLOCK),
'It is not allowed to apply inline attributes to line itself.');
_format(style); _format(style);
} else { } else {
// Otherwise forward to children as it's an inline format update.
assert(style.values.every((attr) => attr.scope == AttributeScope.INLINE)); assert(style.values.every((attr) => attr.scope == AttributeScope.INLINE));
assert(index + local != thisLen); assert(index + local != thisLength);
super.retain(index, local, style); super.retain(index, local, style);
} }
int remain = len - local; final remain = len - local;
if (remain > 0) { if (remain > 0) {
assert(nextLine != null); assert(nextLine != null);
nextLine!.retain(0, remain, style); nextLine!.retain(0, remain, style);
@ -127,66 +149,80 @@ class Line extends Container<Leaf?> {
@override @override
void delete(int index, int? len) { void delete(int index, int? len) {
int local = math.min(length - index, len!); final local = math.min(length - index, len!);
bool deleted = index + local == length; final isLFDeleted = index + local == length; // Line feed
if (deleted) { if (isLFDeleted) {
// Our newline character deleted with all style information.
clearStyle(); clearStyle();
if (local > 1) { if (local > 1) {
// Exclude newline character from delete range for children.
super.delete(index, local - 1); super.delete(index, local - 1);
} }
} else { } else {
super.delete(index, local); super.delete(index, local);
} }
int remain = len - local; final remaining = len - local;
if (remain > 0) { if (remaining > 0) {
assert(nextLine != null); assert(nextLine != null);
nextLine!.delete(0, remain); nextLine!.delete(0, remaining);
} }
if (deleted && isNotEmpty) { if (isLFDeleted && isNotEmpty) {
// Since we lost our line-break and still have child text nodes those must
// migrate to the next line.
// nextLine might have been unmounted since last assert so we need to
// check again we still have a line after us.
assert(nextLine != null); assert(nextLine != null);
// Move remaining children in this line to the next line so that all
// attributes of nextLine are preserved.
nextLine!.moveChildToNewParent(this); nextLine!.moveChildToNewParent(this);
moveChildToNewParent(nextLine); moveChildToNewParent(nextLine);
} }
if (deleted) { if (isLFDeleted) {
Node p = parent!; // Now we can remove this line.
final block = parent!; // remember reference before un-linking.
unlink(); unlink();
p.adjust(); block.adjust();
} }
} }
/// Formats this line.
void _format(Style? newStyle) { void _format(Style? newStyle) {
if (newStyle == null || newStyle.isEmpty) { if (newStyle == null || newStyle.isEmpty) {
return; return;
} }
applyStyle(newStyle); applyStyle(newStyle);
Attribute? blockStyle = newStyle.getBlockExceptHeader(); final blockStyle = newStyle.getBlockExceptHeader();
if (blockStyle == null) { if (blockStyle == null) {
return; return;
} } // No block-level changes
if (parent is Block) { if (parent is Block) {
Attribute? parentStyle = (parent as Block).style.getBlockExceptHeader(); final parentStyle = (parent as Block).style.getBlockExceptHeader();
if (blockStyle.value == null) { if (blockStyle.value == null) {
_unwrap(); _unwrap();
} else if (blockStyle != parentStyle) { } else if (blockStyle != parentStyle) {
_unwrap(); _unwrap();
Block block = Block(); final block = Block()..applyAttribute(blockStyle);
block.applyAttribute(blockStyle);
_wrap(block); _wrap(block);
block.adjust(); block.adjust();
} } // else the same style, no-op.
} else if (blockStyle.value != null) { } else if (blockStyle.value != null) {
Block block = Block(); // Only wrap with a new block if this is not an unset
block.applyAttribute(blockStyle); final block = Block()..applyAttribute(blockStyle);
_wrap(block); _wrap(block);
block.adjust(); block.adjust();
} }
} }
/// Wraps this line with new parent [block].
///
/// This line can not be in a [Block] when this method is called.
void _wrap(Block block) { void _wrap(Block block) {
assert(parent != null && parent is! Block); assert(parent != null && parent is! Block);
insertAfter(block); insertAfter(block);
@ -194,11 +230,14 @@ class Line extends Container<Leaf?> {
block.add(this); block.add(this);
} }
/// Unwraps this line from it's parent [Block].
///
/// This method asserts if current [parent] of this line is not a [Block].
void _unwrap() { void _unwrap() {
if (parent is! Block) { if (parent is! Block) {
throw ArgumentError('Invalid parent'); throw ArgumentError('Invalid parent');
} }
Block block = parent as Block; final block = parent as Block;
assert(block.children.contains(this)); assert(block.children.contains(this));
@ -209,10 +248,10 @@ class Line extends Container<Leaf?> {
unlink(); unlink();
block.insertAfter(this); block.insertAfter(this);
} else { } else {
Block before = block.clone() as Block; final before = block.clone() as Block;
block.insertBefore(before); block.insertBefore(before);
Line child = block.first as Line; var child = block.first as Line;
while (child != this) { while (child != this) {
child.unlink(); child.unlink();
before.add(child); before.add(child);
@ -227,26 +266,25 @@ class Line extends Container<Leaf?> {
Line _getNextLine(int index) { Line _getNextLine(int index) {
assert(index == 0 || (index > 0 && index < length)); assert(index == 0 || (index > 0 && index < length));
Line line = clone() as Line; final line = clone() as Line;
insertAfter(line); insertAfter(line);
if (index == length - 1) { if (index == length - 1) {
return line; return line;
} }
ChildQuery query = queryChild(index, false); final query = queryChild(index, false);
while (!query.node!.isLast) { while (!query.node!.isLast) {
Leaf next = last as Leaf; final next = (last as Leaf)..unlink();
next.unlink();
line.addFirst(next); line.addFirst(next);
} }
Leaf child = query.node as Leaf; final child = query.node as Leaf;
Leaf? cut = child.splitAt(query.offset); final cut = child.splitAt(query.offset);
cut?.unlink(); cut?.unlink();
line.addFirst(cut); line.addFirst(cut);
return line; return line;
} }
void _insert(int index, Object data, Style? style) { void _insertSafe(int index, Object data, Style? style) {
assert(index == 0 || (index > 0 && index < length)); assert(index == 0 || (index > 0 && index < length));
if (data is String) { if (data is String) {
@ -256,47 +294,51 @@ class Line extends Container<Leaf?> {
} }
} }
if (isNotEmpty) { if (isEmpty) {
ChildQuery result = queryChild(index, true); final child = Leaf(data);
add(child);
child.format(style);
} else {
final result = queryChild(index, true);
result.node!.insert(result.offset, data, style); result.node!.insert(result.offset, data, style);
return;
} }
Leaf child = Leaf(data);
add(child);
child.format(style);
}
@override
Node newInstance() {
return Line();
} }
/// Returns style for specified text range.
///
/// Only attributes applied to all characters within this range are
/// included in the result. Inline and line level attributes are
/// handled separately, e.g.:
///
/// - line attribute X is included in the result only if it exists for
/// every line within this range (partially included lines are counted).
/// - inline attribute X is included in the result only if it exists
/// for every character within this range (line-break characters excluded).
Style collectStyle(int offset, int len) { Style collectStyle(int offset, int len) {
int local = math.min(length - offset, len); final local = math.min(length - offset, len);
Style res = Style(); var result = Style();
var excluded = <Attribute>{}; final excluded = <Attribute>{};
void _handle(Style style) { void _handle(Style style) {
if (res.isEmpty) { if (result.isEmpty) {
excluded.addAll(style.values); excluded.addAll(style.values);
} else { } else {
for (Attribute attr in res.values) { for (final attr in result.values) {
if (!style.containsKey(attr.key)) { if (!style.containsKey(attr.key)) {
excluded.add(attr); excluded.add(attr);
} }
} }
} }
Style remain = style.removeAll(excluded); final remaining = style.removeAll(excluded);
res = res.removeAll(excluded); result = result.removeAll(excluded);
res = res.mergeAll(remain); result = result.mergeAll(remaining);
} }
ChildQuery data = queryChild(offset, true); final data = queryChild(offset, true);
Leaf? node = data.node as Leaf?; var node = data.node as Leaf?;
if (node != null) { if (node != null) {
res = res.mergeAll(node.style); result = result.mergeAll(node.style);
int pos = node.length - data.offset; var pos = node.length - data.offset;
while (!node!.isLast && pos < local) { while (!node!.isLast && pos < local) {
node = node.next as Leaf?; node = node.next as Leaf?;
_handle(node!.style); _handle(node!.style);
@ -304,17 +346,18 @@ class Line extends Container<Leaf?> {
} }
} }
res = res.mergeAll(style); result = result.mergeAll(style);
if (parent is Block) { if (parent is Block) {
Block block = parent as Block; final block = parent as Block;
res = res.mergeAll(block.style); result = result.mergeAll(block.style);
} }
int remain = len - local; final remaining = len - local;
if (remain > 0) { if (remaining > 0) {
_handle(nextLine!.collectStyle(0, remain)); final rest = nextLine!.collectStyle(0, remaining);
_handle(rest);
} }
return res; return result;
} }
} }

@ -1,51 +1,49 @@
import 'dart:collection'; import 'dart:collection';
import 'package:flutter_quill/models/documents/style.dart'; import '../../quill_delta.dart';
import 'package:flutter_quill/models/quill_delta.dart';
import '../attribute.dart'; import '../attribute.dart';
import '../style.dart';
import 'container.dart'; import 'container.dart';
import 'line.dart'; import 'line.dart';
/* node in a document tree */ /// An abstract node in a document tree.
///
/// Represents a segment of a Quill document with specified [offset]
/// and [length].
///
/// The [offset] property is relative to [parent]. See also [documentOffset]
/// which provides absolute offset of this node within the document.
///
/// The current parent node is exposed by the [parent] property.
abstract class Node extends LinkedListEntry<Node> { abstract class Node extends LinkedListEntry<Node> {
/// Current parent of this node. May be null if this node is not mounted.
Container? parent; Container? parent;
Style _style = Style();
Style get style => _style; Style get style => _style;
Style _style = Style();
void applyAttribute(Attribute attribute) { /// Returns `true` if this node is the first node in the [parent] list.
_style = _style.merge(attribute);
}
void applyStyle(Style value) {
_style = _style.mergeAll(value);
}
void clearStyle() {
_style = Style();
}
bool get isFirst => list!.first == this; bool get isFirst => list!.first == this;
/// Returns `true` if this node is the last node in the [parent] list.
bool get isLast => list!.last == this; bool get isLast => list!.last == this;
/// Length of this node in characters.
int get length; int get length;
Node clone() { Node clone() => newInstance()..applyStyle(style);
Node node = newInstance();
node.applyStyle(style);
return node;
}
int getOffset() { /// Offset in characters of this node relative to [parent] node.
int offset = 0; ///
/// To get offset of this node in the document see [documentOffset].
int get offset {
var offset = 0;
if (list == null || isFirst) { if (list == null || isFirst) {
return offset; return offset;
} }
Node cur = this; var cur = this;
do { do {
cur = cur.previous!; cur = cur.previous!;
offset += cur.length; offset += cur.length;
@ -53,16 +51,31 @@ abstract class Node extends LinkedListEntry<Node> {
return offset; return offset;
} }
int getDocumentOffset() { /// Offset in characters of this node in the document.
final parentOffset = (parent is! Root) ? parent!.getDocumentOffset() : 0; int get documentOffset {
return parentOffset + getOffset(); final parentOffset = (parent is! Root) ? parent!.documentOffset : 0;
return parentOffset + offset;
} }
/// Returns `true` if this node contains character at specified [offset] in
/// the document.
bool containsOffset(int offset) { bool containsOffset(int offset) {
final o = getDocumentOffset(); final o = documentOffset;
return o <= offset && offset < o + length; return o <= offset && offset < o + length;
} }
void applyAttribute(Attribute attribute) {
_style = _style.merge(attribute);
}
void applyStyle(Style value) {
_style = _style.mergeAll(value);
}
void clearStyle() {
_style = Style();
}
@override @override
void insertBefore(Node entry) { void insertBefore(Node entry) {
assert(entry.parent == null && parent != null); assert(entry.parent == null && parent != null);
@ -84,9 +97,7 @@ abstract class Node extends LinkedListEntry<Node> {
super.unlink(); super.unlink();
} }
void adjust() { void adjust() {/* no-op */}
// do nothing
}
/// abstract methods begin /// abstract methods begin
@ -103,11 +114,13 @@ abstract class Node extends LinkedListEntry<Node> {
void delete(int index, int? len); void delete(int index, int? len);
/// abstract methods end /// abstract methods end
} }
/* Root node of document tree */ /// Root node of document tree.
class Root extends Container<Container<Node?>> { class Root extends Container<Container<Node?>> {
@override
Node newInstance() => Root();
@override @override
Container<Node?> get defaultChild => Line(); Container<Node?> get defaultChild => Line();
@ -115,9 +128,4 @@ class Root extends Container<Container<Node?>> {
Delta toDelta() => children Delta toDelta() => children
.map((child) => child.toDelta()) .map((child) => child.toDelta())
.fold(Delta(), (a, b) => a.concat(b)); .fold(Delta(), (a, b) => a.concat(b));
@override
Node newInstance() {
return Root();
}
} }

@ -1,22 +1,23 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:quiver/core.dart'; import 'package:quiver/core.dart';
import 'attribute.dart';
/* Collection of style attributes */ /* Collection of style attributes */
class Style { class Style {
final Map<String, Attribute> _attributes; Style() : _attributes = <String, Attribute>{};
Style.attr(this._attributes); Style.attr(this._attributes);
Style() : _attributes = <String, Attribute>{}; final Map<String, Attribute> _attributes;
static Style fromJson(Map<String, dynamic>? attributes) { static Style fromJson(Map<String, dynamic>? attributes) {
if (attributes == null) { if (attributes == null) {
return Style(); return Style();
} }
Map<String, Attribute> result = attributes.map((String key, dynamic value) { final result = attributes.map((key, dynamic value) {
Attribute attr = Attribute.fromKeyValue(key, value); final attr = Attribute.fromKeyValue(key, value);
return MapEntry<String, Attribute>(key, attr); return MapEntry<String, Attribute>(key, attr);
}); });
return Style.attr(result); return Style.attr(result);
@ -24,7 +25,7 @@ class Style {
Map<String, dynamic>? toJson() => _attributes.isEmpty Map<String, dynamic>? toJson() => _attributes.isEmpty
? null ? null
: _attributes.map<String, dynamic>((String _, Attribute attribute) => : _attributes.map<String, dynamic>((_, attribute) =>
MapEntry<String, dynamic>(attribute.key, attribute.value)); MapEntry<String, dynamic>(attribute.key, attribute.value));
Iterable<String> get keys => _attributes.keys; Iterable<String> get keys => _attributes.keys;
@ -47,7 +48,7 @@ class Style {
bool containsKey(String key) => _attributes.containsKey(key); bool containsKey(String key) => _attributes.containsKey(key);
Attribute? getBlockExceptHeader() { Attribute? getBlockExceptHeader() {
for (Attribute val in values) { for (final val in values) {
if (val.isBlockExceptHeader) { if (val.isBlockExceptHeader) {
return val; return val;
} }
@ -56,7 +57,7 @@ class Style {
} }
Style merge(Attribute attribute) { Style merge(Attribute attribute) {
Map<String, Attribute> merged = Map<String, Attribute>.from(_attributes); final merged = Map<String, Attribute>.from(_attributes);
if (attribute.value == null) { if (attribute.value == null) {
merged.remove(attribute.key); merged.remove(attribute.key);
} else { } else {
@ -66,21 +67,21 @@ class Style {
} }
Style mergeAll(Style other) { Style mergeAll(Style other) {
Style result = Style.attr(_attributes); var result = Style.attr(_attributes);
for (Attribute attribute in other.values) { for (final attribute in other.values) {
result = result.merge(attribute); result = result.merge(attribute);
} }
return result; return result;
} }
Style removeAll(Set<Attribute> attributes) { Style removeAll(Set<Attribute> attributes) {
Map<String, Attribute> merged = Map<String, Attribute>.from(_attributes); final merged = Map<String, Attribute>.from(_attributes);
attributes.map((item) => item.key).forEach(merged.remove); attributes.map((item) => item.key).forEach(merged.remove);
return Style.attr(merged); return Style.attr(merged);
} }
Style put(Attribute attribute) { Style put(Attribute attribute) {
Map<String, Attribute> m = Map<String, Attribute>.from(attributes); final m = Map<String, Attribute>.from(attributes);
m[attribute.key] = attribute; m[attribute.key] = attribute;
return Style.attr(m); return Style.attr(m);
} }
@ -93,8 +94,8 @@ class Style {
if (other is! Style) { if (other is! Style) {
return false; return false;
} }
Style typedOther = other; final typedOther = other;
final eq = const MapEquality<String, Attribute>(); const eq = MapEquality<String, Attribute>();
return eq.equals(_attributes, typedOther._attributes); return eq.equals(_attributes, typedOther._attributes);
} }

@ -22,6 +22,29 @@ Object? _passThroughDataDecoder(Object? data) => data;
/// Operation performed on a rich-text document. /// Operation performed on a rich-text document.
class Operation { class Operation {
Operation._(this.key, this.length, this.data, Map? attributes)
: assert(_validKeys.contains(key), 'Invalid operation key "$key".'),
assert(() {
if (key != Operation.insertKey) return true;
return data is String ? data.length == length : length == 1;
}(), 'Length of insert operation must be equal to the data length.'),
_attributes =
attributes != null ? Map<String, dynamic>.from(attributes) : null;
/// Creates operation which deletes [length] of characters.
factory Operation.delete(int length) =>
Operation._(Operation.deleteKey, length, '', null);
/// Creates operation which inserts [text] with optional [attributes].
factory Operation.insert(dynamic data, [Map<String, dynamic>? attributes]) =>
Operation._(Operation.insertKey, data is String ? data.length : 1, data,
attributes);
/// Creates operation which retains [length] of characters and optionally
/// applies attributes.
factory Operation.retain(int? length, [Map<String, dynamic>? attributes]) =>
Operation._(Operation.retainKey, length, '', attributes);
/// Key of insert operations. /// Key of insert operations.
static const String insertKey = 'insert'; static const String insertKey = 'insert';
@ -50,15 +73,6 @@ class Operation {
_attributes == null ? null : Map<String, dynamic>.from(_attributes!); _attributes == null ? null : Map<String, dynamic>.from(_attributes!);
final Map<String, dynamic>? _attributes; final Map<String, dynamic>? _attributes;
Operation._(this.key, this.length, this.data, Map? attributes)
: assert(_validKeys.contains(key), 'Invalid operation key "$key".'),
assert(() {
if (key != Operation.insertKey) return true;
return data is String ? data.length == length : length == 1;
}(), 'Length of insert operation must be equal to the data length.'),
_attributes =
attributes != null ? Map<String, dynamic>.from(attributes) : null;
/// Creates new [Operation] from JSON payload. /// Creates new [Operation] from JSON payload.
/// ///
/// If `dataDecoder` parameter is not null then it is used to additionally /// If `dataDecoder` parameter is not null then it is used to additionally
@ -89,20 +103,6 @@ class Operation {
return json; return json;
} }
/// Creates operation which deletes [length] of characters.
factory Operation.delete(int length) =>
Operation._(Operation.deleteKey, length, '', null);
/// Creates operation which inserts [text] with optional [attributes].
factory Operation.insert(dynamic data, [Map<String, dynamic>? attributes]) =>
Operation._(Operation.insertKey, data is String ? data.length : 1, data,
attributes);
/// Creates operation which retains [length] of characters and optionally
/// applies attributes.
factory Operation.retain(int? length, [Map<String, dynamic>? attributes]) =>
Operation._(Operation.retainKey, length, '', attributes);
/// Returns value of this operation. /// Returns value of this operation.
/// ///
/// For insert operations this returns text, for delete and retain - length. /// For insert operations this returns text, for delete and retain - length.
@ -135,7 +135,7 @@ class Operation {
bool operator ==(other) { bool operator ==(other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
if (other is! Operation) return false; if (other is! Operation) return false;
Operation typedOther = other; final typedOther = other;
return key == typedOther.key && return key == typedOther.key &&
length == typedOther.length && length == typedOther.length &&
_valueEquality.equals(data, typedOther.data) && _valueEquality.equals(data, typedOther.data) &&
@ -180,6 +180,15 @@ class Operation {
/// "document delta". When delta includes also "retain" or "delete" operations /// "document delta". When delta includes also "retain" or "delete" operations
/// it is a "change delta". /// it is a "change delta".
class Delta { class Delta {
/// Creates new empty [Delta].
factory Delta() => Delta._(<Operation>[]);
Delta._(List<Operation> operations) : _operations = operations;
/// Creates new [Delta] from [other].
factory Delta.from(Delta other) =>
Delta._(List<Operation>.from(other._operations));
/// Transforms two attribute sets. /// Transforms two attribute sets.
static Map<String, dynamic>? transformAttributes( static Map<String, dynamic>? transformAttributes(
Map<String, dynamic>? a, Map<String, dynamic>? b, bool priority) { Map<String, dynamic>? a, Map<String, dynamic>? b, bool priority) {
@ -221,14 +230,14 @@ class Delta {
attr ??= const {}; attr ??= const {};
base ??= const {}; base ??= const {};
var baseInverted = base.keys.fold({}, (dynamic memo, key) { final baseInverted = base.keys.fold({}, (dynamic memo, key) {
if (base![key] != attr![key] && attr.containsKey(key)) { if (base![key] != attr![key] && attr.containsKey(key)) {
memo[key] = base[key]; memo[key] = base[key];
} }
return memo; return memo;
}); });
var inverted = final inverted =
Map<String, dynamic>.from(attr.keys.fold(baseInverted, (memo, key) { Map<String, dynamic>.from(attr.keys.fold(baseInverted, (memo, key) {
if (base![key] != attr![key] && !base.containsKey(key)) { if (base![key] != attr![key] && !base.containsKey(key)) {
memo[key] = null; memo[key] = null;
@ -242,15 +251,6 @@ class Delta {
int _modificationCount = 0; int _modificationCount = 0;
Delta._(List<Operation> operations) : _operations = operations;
/// Creates new empty [Delta].
factory Delta() => Delta._(<Operation>[]);
/// Creates new [Delta] from [other].
factory Delta.from(Delta other) =>
Delta._(List<Operation>.from(other._operations));
/// Creates [Delta] from de-serialized JSON representation. /// Creates [Delta] from de-serialized JSON representation.
/// ///
/// If `dataDecoder` parameter is not null then it is used to additionally /// If `dataDecoder` parameter is not null then it is used to additionally
@ -292,9 +292,8 @@ class Delta {
bool operator ==(dynamic other) { bool operator ==(dynamic other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
if (other is! Delta) return false; if (other is! Delta) return false;
Delta typedOther = other; final typedOther = other;
final comparator = const comparator = ListEquality<Operation>(DefaultEquality<Operation>());
const ListEquality<Operation>(DefaultEquality<Operation>());
return comparator.equals(_operations, typedOther._operations); return comparator.equals(_operations, typedOther._operations);
} }
@ -529,7 +528,8 @@ class Delta {
if (op.isDelete) { if (op.isDelete) {
inverted.push(baseOp); inverted.push(baseOp);
} else if (op.isRetain && op.isNotPlain) { } else if (op.isRetain && op.isNotPlain) {
var invertAttr = invertAttributes(op.attributes, baseOp.attributes); final invertAttr =
invertAttributes(op.attributes, baseOp.attributes);
inverted.retain( inverted.retain(
baseOp.length!, invertAttr.isEmpty ? null : invertAttr); baseOp.length!, invertAttr.isEmpty ? null : invertAttr);
} }
@ -548,7 +548,7 @@ class Delta {
Delta slice(int start, [int? end]) { Delta slice(int start, [int? end]) {
final delta = Delta(); final delta = Delta();
var index = 0; var index = 0;
var opIterator = DeltaIterator(this); final opIterator = DeltaIterator(this);
final actualEnd = end ?? double.infinity; final actualEnd = end ?? double.infinity;
@ -599,13 +599,13 @@ class Delta {
/// Specialized iterator for [Delta]s. /// Specialized iterator for [Delta]s.
class DeltaIterator { class DeltaIterator {
DeltaIterator(this.delta) : _modificationCount = delta._modificationCount;
final Delta delta; final Delta delta;
final int _modificationCount; final int _modificationCount;
int _index = 0; int _index = 0;
num _offset = 0; num _offset = 0;
DeltaIterator(this.delta) : _modificationCount = delta._modificationCount;
bool get isNextInsert => nextOperationKey == Operation.insertKey; bool get isNextInsert => nextOperationKey == Operation.insertKey;
bool get isNextDelete => nextOperationKey == Operation.deleteKey; bool get isNextDelete => nextOperationKey == Operation.deleteKey;
@ -661,7 +661,7 @@ class DeltaIterator {
final opIsNotEmpty = final opIsNotEmpty =
opData is String ? opData.isNotEmpty : true; // embeds are never empty opData is String ? opData.isNotEmpty : true; // embeds are never empty
final opLength = opData is String ? opData.length : 1; final opLength = opData is String ? opData.length : 1;
final int opActualLength = opIsNotEmpty ? opLength : actualLength as int; final opActualLength = opIsNotEmpty ? opLength : actualLength as int;
return Operation._(opKey, opActualLength, opData, opAttributes); return Operation._(opKey, opActualLength, opData, opAttributes);
} }
return Operation.retain(length); return Operation.retain(length);

@ -1,6 +1,6 @@
import 'package:flutter_quill/models/documents/attribute.dart'; import '../documents/attribute.dart';
import 'package:flutter_quill/models/quill_delta.dart'; import '../quill_delta.dart';
import 'package:flutter_quill/models/rules/rule.dart'; import 'rule.dart';
abstract class DeleteRule extends Rule { abstract class DeleteRule extends Rule {
const DeleteRule(); const DeleteRule();
@ -34,34 +34,33 @@ class PreserveLineStyleOnMergeRule extends DeleteRule {
@override @override
Delta? applyRule(Delta document, int index, Delta? applyRule(Delta document, int index,
{int? len, Object? data, Attribute? attribute}) { {int? len, Object? data, Attribute? attribute}) {
DeltaIterator itr = DeltaIterator(document); final itr = DeltaIterator(document)..skip(index);
itr.skip(index); var op = itr.next(1);
Operation op = itr.next(1);
if (op.data != '\n') { if (op.data != '\n') {
return null; return null;
} }
bool isNotPlain = op.isNotPlain; final isNotPlain = op.isNotPlain;
Map<String, dynamic>? attrs = op.attributes; final attrs = op.attributes;
itr.skip(len! - 1); itr.skip(len! - 1);
Delta delta = Delta() final delta = Delta()
..retain(index) ..retain(index)
..delete(len); ..delete(len);
while (itr.hasNext) { while (itr.hasNext) {
op = itr.next(); op = itr.next();
String text = op.data is String ? (op.data as String?)! : ''; final text = op.data is String ? (op.data as String?)! : '';
int lineBreak = text.indexOf('\n'); final lineBreak = text.indexOf('\n');
if (lineBreak == -1) { if (lineBreak == -1) {
delta.retain(op.length!); delta.retain(op.length!);
continue; continue;
} }
Map<String, dynamic>? attributes = op.attributes == null var attributes = op.attributes == null
? null ? null
: op.attributes!.map<String, dynamic>((String key, dynamic value) => : op.attributes!.map<String, dynamic>(
MapEntry<String, dynamic>(key, null)); (key, dynamic value) => MapEntry<String, dynamic>(key, null));
if (isNotPlain) { if (isNotPlain) {
attributes ??= <String, dynamic>{}; attributes ??= <String, dynamic>{};
@ -80,15 +79,15 @@ class EnsureEmbedLineRule extends DeleteRule {
@override @override
Delta? applyRule(Delta document, int index, Delta? applyRule(Delta document, int index,
{int? len, Object? data, Attribute? attribute}) { {int? len, Object? data, Attribute? attribute}) {
DeltaIterator itr = DeltaIterator(document); final itr = DeltaIterator(document);
Operation? op = itr.skip(index); var op = itr.skip(index);
int? indexDelta = 0, lengthDelta = 0, remain = len; int? indexDelta = 0, lengthDelta = 0, remain = len;
bool embedFound = op != null && op.data is! String; var embedFound = op != null && op.data is! String;
bool hasLineBreakBefore = final hasLineBreakBefore =
!embedFound && (op == null || (op.data as String).endsWith('\n')); !embedFound && (op == null || (op.data as String).endsWith('\n'));
if (embedFound) { if (embedFound) {
Operation candidate = itr.next(1); var candidate = itr.next(1);
if (remain != null) { if (remain != null) {
remain--; remain--;
if (candidate.data == '\n') { if (candidate.data == '\n') {
@ -107,7 +106,7 @@ class EnsureEmbedLineRule extends DeleteRule {
op = itr.skip(remain!); op = itr.skip(remain!);
if (op != null && if (op != null &&
(op.data is String ? op.data as String? : '')!.endsWith('\n')) { (op.data is String ? op.data as String? : '')!.endsWith('\n')) {
Operation candidate = itr.next(1); final candidate = itr.next(1);
if (candidate.data is! String && !hasLineBreakBefore) { if (candidate.data is! String && !hasLineBreakBefore) {
embedFound = true; embedFound = true;
lengthDelta--; lengthDelta--;

@ -1,6 +1,6 @@
import 'package:flutter_quill/models/documents/attribute.dart'; import '../documents/attribute.dart';
import 'package:flutter_quill/models/quill_delta.dart'; import '../quill_delta.dart';
import 'package:flutter_quill/models/rules/rule.dart'; import 'rule.dart';
abstract class FormatRule extends Rule { abstract class FormatRule extends Rule {
const FormatRule(); const FormatRule();
@ -26,21 +26,20 @@ class ResolveLineFormatRule extends FormatRule {
return null; return null;
} }
Delta delta = Delta()..retain(index); var delta = Delta()..retain(index);
DeltaIterator itr = DeltaIterator(document); final itr = DeltaIterator(document)..skip(index);
itr.skip(index);
Operation op; Operation op;
for (int cur = 0; cur < len! && itr.hasNext; cur += op.length!) { for (var cur = 0; cur < len! && itr.hasNext; cur += op.length!) {
op = itr.next(len - cur); op = itr.next(len - cur);
if (op.data is! String || !(op.data as String).contains('\n')) { if (op.data is! String || !(op.data as String).contains('\n')) {
delta.retain(op.length!); delta.retain(op.length!);
continue; continue;
} }
String text = op.data as String; final text = op.data as String;
Delta tmp = Delta(); final tmp = Delta();
int offset = 0; var offset = 0;
for (int lineBreak = text.indexOf('\n'); for (var lineBreak = text.indexOf('\n');
lineBreak >= 0; lineBreak >= 0;
lineBreak = text.indexOf('\n', offset)) { lineBreak = text.indexOf('\n', offset)) {
tmp..retain(lineBreak - offset)..retain(1, attribute.toJson()); tmp..retain(lineBreak - offset)..retain(1, attribute.toJson());
@ -52,8 +51,8 @@ class ResolveLineFormatRule extends FormatRule {
while (itr.hasNext) { while (itr.hasNext) {
op = itr.next(); op = itr.next();
String text = op.data is String ? (op.data as String?)! : ''; final text = op.data is String ? (op.data as String?)! : '';
int lineBreak = text.indexOf('\n'); final lineBreak = text.indexOf('\n');
if (lineBreak < 0) { if (lineBreak < 0) {
delta.retain(op.length!); delta.retain(op.length!);
continue; continue;
@ -75,9 +74,9 @@ class FormatLinkAtCaretPositionRule extends FormatRule {
return null; return null;
} }
Delta delta = Delta(); final delta = Delta();
DeltaIterator itr = DeltaIterator(document); final itr = DeltaIterator(document);
Operation? before = itr.skip(index), after = itr.next(); final before = itr.skip(index), after = itr.next();
int? beg = index, retain = 0; int? beg = index, retain = 0;
if (before != null && before.hasAttribute(attribute.key)) { if (before != null && before.hasAttribute(attribute.key)) {
beg -= before.length!; beg -= before.length!;
@ -105,20 +104,19 @@ class ResolveInlineFormatRule extends FormatRule {
return null; return null;
} }
Delta delta = Delta()..retain(index); final delta = Delta()..retain(index);
DeltaIterator itr = DeltaIterator(document); final itr = DeltaIterator(document)..skip(index);
itr.skip(index);
Operation op; Operation op;
for (int cur = 0; cur < len! && itr.hasNext; cur += op.length!) { for (var cur = 0; cur < len! && itr.hasNext; cur += op.length!) {
op = itr.next(len - cur); op = itr.next(len - cur);
String text = op.data is String ? (op.data as String?)! : ''; final text = op.data is String ? (op.data as String?)! : '';
int lineBreak = text.indexOf('\n'); var lineBreak = text.indexOf('\n');
if (lineBreak < 0) { if (lineBreak < 0) {
delta.retain(op.length!, attribute.toJson()); delta.retain(op.length!, attribute.toJson());
continue; continue;
} }
int pos = 0; var pos = 0;
while (lineBreak >= 0) { while (lineBreak >= 0) {
delta..retain(lineBreak - pos, attribute.toJson())..retain(1); delta..retain(lineBreak - pos, attribute.toJson())..retain(1);
pos = lineBreak + 1; pos = lineBreak + 1;

@ -1,9 +1,10 @@
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/documents/style.dart';
import 'package:flutter_quill/models/quill_delta.dart';
import 'package:flutter_quill/models/rules/rule.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import '../documents/attribute.dart';
import '../documents/style.dart';
import '../quill_delta.dart';
import 'rule.dart';
abstract class InsertRule extends Rule { abstract class InsertRule extends Rule {
const InsertRule(); const InsertRule();
@ -28,28 +29,28 @@ class PreserveLineStyleOnSplitRule extends InsertRule {
return null; return null;
} }
DeltaIterator itr = DeltaIterator(document); final itr = DeltaIterator(document);
Operation? before = itr.skip(index); final before = itr.skip(index);
if (before == null || if (before == null ||
before.data is! String || before.data is! String ||
(before.data as String).endsWith('\n')) { (before.data as String).endsWith('\n')) {
return null; return null;
} }
Operation after = itr.next(); final after = itr.next();
if (after.data is! String || (after.data as String).startsWith('\n')) { if (after.data is! String || (after.data as String).startsWith('\n')) {
return null; return null;
} }
final text = after.data as String; final text = after.data as String;
Delta delta = Delta()..retain(index); final delta = Delta()..retain(index);
if (text.contains('\n')) { if (text.contains('\n')) {
assert(after.isPlain); assert(after.isPlain);
delta.insert('\n'); delta.insert('\n');
return delta; return delta;
} }
Tuple2<Operation?, int?> nextNewLine = _getNextNewLine(itr); final nextNewLine = _getNextNewLine(itr);
Map<String, dynamic>? attributes = nextNewLine.item1?.attributes; final attributes = nextNewLine.item1?.attributes;
return delta..insert('\n', attributes); return delta..insert('\n', attributes);
} }
@ -65,19 +66,18 @@ class PreserveBlockStyleOnInsertRule extends InsertRule {
return null; return null;
} }
DeltaIterator itr = DeltaIterator(document); final itr = DeltaIterator(document)..skip(index);
itr.skip(index);
Tuple2<Operation?, int?> nextNewLine = _getNextNewLine(itr); final nextNewLine = _getNextNewLine(itr);
Style lineStyle = final lineStyle =
Style.fromJson(nextNewLine.item1?.attributes ?? <String, dynamic>{}); Style.fromJson(nextNewLine.item1?.attributes ?? <String, dynamic>{});
Attribute? attribute = lineStyle.getBlockExceptHeader(); final attribute = lineStyle.getBlockExceptHeader();
if (attribute == null) { if (attribute == null) {
return null; return null;
} }
var blockStyle = <String, dynamic>{attribute.key: attribute.value}; final blockStyle = <String, dynamic>{attribute.key: attribute.value};
Map<String, dynamic>? resetStyle; Map<String, dynamic>? resetStyle;
@ -85,10 +85,10 @@ class PreserveBlockStyleOnInsertRule extends InsertRule {
resetStyle = Attribute.header.toJson(); resetStyle = Attribute.header.toJson();
} }
List<String> lines = data.split('\n'); final lines = data.split('\n');
Delta delta = Delta()..retain(index); final delta = Delta()..retain(index);
for (int i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
String line = lines[i]; final line = lines[i];
if (line.isNotEmpty) { if (line.isNotEmpty) {
delta.insert(line); delta.insert(line);
} }
@ -100,8 +100,8 @@ class PreserveBlockStyleOnInsertRule extends InsertRule {
} }
if (resetStyle != null) { if (resetStyle != null) {
delta.retain(nextNewLine.item2!);
delta delta
..retain(nextNewLine.item2!)
..retain((nextNewLine.item1!.data as String).indexOf('\n')) ..retain((nextNewLine.item1!.data as String).indexOf('\n'))
..retain(1, resetStyle); ..retain(1, resetStyle);
} }
@ -130,10 +130,9 @@ class AutoExitBlockRule extends InsertRule {
return null; return null;
} }
DeltaIterator itr = DeltaIterator(document); final itr = DeltaIterator(document);
Operation? prev = itr.skip(index), cur = itr.next(); final prev = itr.skip(index), cur = itr.next();
Attribute? blockStyle = final blockStyle = Style.fromJson(cur.attributes).getBlockExceptHeader();
Style.fromJson(cur.attributes).getBlockExceptHeader();
if (cur.isPlain || blockStyle == null) { if (cur.isPlain || blockStyle == null) {
return null; return null;
} }
@ -145,7 +144,7 @@ class AutoExitBlockRule extends InsertRule {
return null; return null;
} }
Tuple2<Operation?, int?> nextNewLine = _getNextNewLine(itr); final nextNewLine = _getNextNewLine(itr);
if (nextNewLine.item1 != null && if (nextNewLine.item1 != null &&
nextNewLine.item1!.attributes != null && nextNewLine.item1!.attributes != null &&
Style.fromJson(nextNewLine.item1!.attributes).getBlockExceptHeader() == Style.fromJson(nextNewLine.item1!.attributes).getBlockExceptHeader() ==
@ -154,7 +153,7 @@ class AutoExitBlockRule extends InsertRule {
} }
final attributes = cur.attributes ?? <String, dynamic>{}; final attributes = cur.attributes ?? <String, dynamic>{};
String k = attributes.keys final k = attributes.keys
.firstWhere((k) => Attribute.blockKeysExceptHeader.contains(k)); .firstWhere((k) => Attribute.blockKeysExceptHeader.contains(k));
attributes[k] = null; attributes[k] = null;
// retain(1) should be '\n', set it with no attribute // retain(1) should be '\n', set it with no attribute
@ -172,9 +171,8 @@ class ResetLineFormatOnNewLineRule extends InsertRule {
return null; return null;
} }
DeltaIterator itr = DeltaIterator(document); final itr = DeltaIterator(document)..skip(index);
itr.skip(index); final cur = itr.next();
Operation cur = itr.next();
if (cur.data is! String || !(cur.data as String).startsWith('\n')) { if (cur.data is! String || !(cur.data as String).startsWith('\n')) {
return null; return null;
} }
@ -202,12 +200,12 @@ class InsertEmbedsRule extends InsertRule {
return null; return null;
} }
Delta delta = Delta()..retain(index); final delta = Delta()..retain(index);
DeltaIterator itr = DeltaIterator(document); final itr = DeltaIterator(document);
Operation? prev = itr.skip(index), cur = itr.next(); final prev = itr.skip(index), cur = itr.next();
String? textBefore = prev?.data is String ? prev!.data as String? : ''; final textBefore = prev?.data is String ? prev!.data as String? : '';
String textAfter = cur.data is String ? (cur.data as String?)! : ''; final textAfter = cur.data is String ? (cur.data as String?)! : '';
final isNewlineBefore = prev == null || textBefore!.endsWith('\n'); final isNewlineBefore = prev == null || textBefore!.endsWith('\n');
final isNewlineAfter = textAfter.startsWith('\n'); final isNewlineAfter = textAfter.startsWith('\n');
@ -221,7 +219,7 @@ class InsertEmbedsRule extends InsertRule {
lineStyle = cur.attributes; lineStyle = cur.attributes;
} else { } else {
while (itr.hasNext) { while (itr.hasNext) {
Operation op = itr.next(); final op = itr.next();
if ((op.data is String ? op.data as String? : '')!.contains('\n')) { if ((op.data is String ? op.data as String? : '')!.contains('\n')) {
lineStyle = op.attributes; lineStyle = op.attributes;
break; break;
@ -250,17 +248,17 @@ class ForceNewlineForInsertsAroundEmbedRule extends InsertRule {
return null; return null;
} }
String text = data; final text = data;
DeltaIterator itr = DeltaIterator(document); final itr = DeltaIterator(document);
final prev = itr.skip(index); final prev = itr.skip(index);
final cur = itr.next(); final cur = itr.next();
bool cursorBeforeEmbed = cur.data is! String; final cursorBeforeEmbed = cur.data is! String;
bool cursorAfterEmbed = prev != null && prev.data is! String; final cursorAfterEmbed = prev != null && prev.data is! String;
if (!cursorBeforeEmbed && !cursorAfterEmbed) { if (!cursorBeforeEmbed && !cursorAfterEmbed) {
return null; return null;
} }
Delta delta = Delta()..retain(index); final delta = Delta()..retain(index);
if (cursorBeforeEmbed && !text.endsWith('\n')) { if (cursorBeforeEmbed && !text.endsWith('\n')) {
return delta..insert(text)..insert('\n'); return delta..insert(text)..insert('\n');
} }
@ -281,19 +279,19 @@ class AutoFormatLinksRule extends InsertRule {
return null; return null;
} }
DeltaIterator itr = DeltaIterator(document); final itr = DeltaIterator(document);
Operation? prev = itr.skip(index); final prev = itr.skip(index);
if (prev == null || prev.data is! String) { if (prev == null || prev.data is! String) {
return null; return null;
} }
try { try {
String cand = (prev.data as String).split('\n').last.split(' ').last; final cand = (prev.data as String).split('\n').last.split(' ').last;
Uri link = Uri.parse(cand); final link = Uri.parse(cand);
if (!['https', 'http'].contains(link.scheme)) { if (!['https', 'http'].contains(link.scheme)) {
return null; return null;
} }
Map<String, dynamic> attributes = prev.attributes ?? <String, dynamic>{}; final attributes = prev.attributes ?? <String, dynamic>{};
if (attributes.containsKey(Attribute.link.key)) { if (attributes.containsKey(Attribute.link.key)) {
return null; return null;
@ -320,16 +318,16 @@ class PreserveInlineStylesRule extends InsertRule {
return null; return null;
} }
DeltaIterator itr = DeltaIterator(document); final itr = DeltaIterator(document);
Operation? prev = itr.skip(index); final prev = itr.skip(index);
if (prev == null || if (prev == null ||
prev.data is! String || prev.data is! String ||
(prev.data as String).contains('\n')) { (prev.data as String).contains('\n')) {
return null; return null;
} }
Map<String, dynamic>? attributes = prev.attributes; final attributes = prev.attributes;
String text = data; final text = data;
if (attributes == null || !attributes.containsKey(Attribute.link.key)) { if (attributes == null || !attributes.containsKey(Attribute.link.key)) {
return Delta() return Delta()
..retain(index) ..retain(index)
@ -337,13 +335,12 @@ class PreserveInlineStylesRule extends InsertRule {
} }
attributes.remove(Attribute.link.key); attributes.remove(Attribute.link.key);
Delta delta = Delta() final delta = Delta()
..retain(index) ..retain(index)
..insert(text, attributes.isEmpty ? null : attributes); ..insert(text, attributes.isEmpty ? null : attributes);
Operation next = itr.next(); final next = itr.next();
Map<String, dynamic> nextAttributes = final nextAttributes = next.attributes ?? const <String, dynamic>{};
next.attributes ?? const <String, dynamic>{};
if (!nextAttributes.containsKey(Attribute.link.key)) { if (!nextAttributes.containsKey(Attribute.link.key)) {
return delta; return delta;
} }
@ -370,9 +367,9 @@ class CatchAllInsertRule extends InsertRule {
Tuple2<Operation?, int?> _getNextNewLine(DeltaIterator iterator) { Tuple2<Operation?, int?> _getNextNewLine(DeltaIterator iterator) {
Operation op; Operation op;
for (int skipped = 0; iterator.hasNext; skipped += op.length!) { for (var skipped = 0; iterator.hasNext; skipped += op.length!) {
op = iterator.next(); op = iterator.next();
int lineBreak = final lineBreak =
(op.data is String ? op.data as String? : '')!.indexOf('\n'); (op.data is String ? op.data as String? : '')!.indexOf('\n');
if (lineBreak >= 0) { if (lineBreak >= 0) {
return Tuple2(op, skipped); return Tuple2(op, skipped);

@ -1,7 +1,6 @@
import 'package:flutter_quill/models/documents/attribute.dart'; import '../documents/attribute.dart';
import 'package:flutter_quill/models/documents/document.dart'; import '../documents/document.dart';
import 'package:flutter_quill/models/quill_delta.dart'; import '../quill_delta.dart';
import 'delete.dart'; import 'delete.dart';
import 'format.dart'; import 'format.dart';
import 'insert.dart'; import 'insert.dart';
@ -27,6 +26,8 @@ abstract class Rule {
} }
class Rules { class Rules {
Rules(this._rules);
final List<Rule> _rules; final List<Rule> _rules;
static final Rules _instance = Rules([ static final Rules _instance = Rules([
const FormatLinkAtCaretPositionRule(), const FormatLinkAtCaretPositionRule(),
@ -46,14 +47,12 @@ class Rules {
const CatchAllDeleteRule(), const CatchAllDeleteRule(),
]); ]);
Rules(this._rules);
static Rules getInstance() => _instance; static Rules getInstance() => _instance;
Delta apply(RuleType ruleType, Document document, int index, Delta apply(RuleType ruleType, Document document, int index,
{int? len, Object? data, Attribute? attribute}) { {int? len, Object? data, Attribute? attribute}) {
final delta = document.toDelta(); final delta = document.toDelta();
for (var rule in _rules) { for (final rule in _rules) {
if (rule.type != ruleType) { if (rule.type != ruleType) {
continue; continue;
} }

@ -118,8 +118,8 @@ Color stringToColor(String? s) {
throw 'Color code not supported'; throw 'Color code not supported';
} }
String hex = s.replaceFirst('#', ''); var hex = s.replaceFirst('#', '');
hex = hex.length == 6 ? 'ff' + hex : hex; hex = hex.length == 6 ? 'ff$hex' : hex;
int val = int.parse(hex, radix: 16); final val = int.parse(hex, radix: 16);
return Color(val); return Color(val);
} }

@ -1,6 +1,6 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter_quill/models/quill_delta.dart'; import '../models/quill_delta.dart';
const Set<int> WHITE_SPACE = { const Set<int> WHITE_SPACE = {
0x9, 0x9,
@ -33,6 +33,8 @@ const Set<int> WHITE_SPACE = {
// Diff between two texts - old text and new text // Diff between two texts - old text and new text
class Diff { class Diff {
Diff(this.start, this.deleted, this.inserted);
// Start index in old text at which changes begin. // Start index in old text at which changes begin.
final int start; final int start;
@ -42,8 +44,6 @@ class Diff {
// The inserted text // The inserted text
final String inserted; final String inserted;
Diff(this.start, this.deleted, this.inserted);
@override @override
String toString() { String toString() {
return 'Diff[$start, "$deleted", "$inserted"]'; return 'Diff[$start, "$deleted", "$inserted"]';
@ -52,17 +52,17 @@ class Diff {
/* Get diff operation between old text and new text */ /* Get diff operation between old text and new text */
Diff getDiff(String oldText, String newText, int cursorPosition) { Diff getDiff(String oldText, String newText, int cursorPosition) {
int end = oldText.length; var end = oldText.length;
int delta = newText.length - end; final delta = newText.length - end;
for (int limit = math.max(0, cursorPosition - delta); for (final limit = math.max(0, cursorPosition - delta);
end > limit && oldText[end - 1] == newText[end + delta - 1]; end > limit && oldText[end - 1] == newText[end + delta - 1];
end--) {} end--) {}
int start = 0; var start = 0;
for (int startLimit = cursorPosition - math.max(0, delta); for (final startLimit = cursorPosition - math.max(0, delta);
start < startLimit && oldText[start] == newText[start]; start < startLimit && oldText[start] == newText[start];
start++) {} start++) {}
String deleted = (start >= end) ? '' : oldText.substring(start, end); final deleted = (start >= end) ? '' : oldText.substring(start, end);
String inserted = newText.substring(start, end + delta); final inserted = newText.substring(start, end + delta);
return Diff(start, deleted, inserted); return Diff(start, deleted, inserted);
} }
@ -71,19 +71,15 @@ int getPositionDelta(Delta user, Delta actual) {
return 0; return 0;
} }
DeltaIterator userItr = DeltaIterator(user); final userItr = DeltaIterator(user);
DeltaIterator actualItr = DeltaIterator(actual); final actualItr = DeltaIterator(actual);
int diff = 0; var diff = 0;
while (userItr.hasNext || actualItr.hasNext) { while (userItr.hasNext || actualItr.hasNext) {
final length = math.min(userItr.peekLength(), actualItr.peekLength()); final length = math.min(userItr.peekLength(), actualItr.peekLength());
Operation userOperation = userItr.next(length as int); final userOperation = userItr.next(length as int);
Operation actualOperation = actualItr.next(length); final actualOperation = actualItr.next(length);
if (userOperation.length != actualOperation.length) { if (userOperation.length != actualOperation.length) {
throw 'userOp ' + throw 'userOp ${userOperation.length} does not match actualOp ${actualOperation.length}';
userOperation.length.toString() +
' does not match ' +
' actualOp ' +
actualOperation.length.toString();
} }
if (userOperation.key == actualOperation.key) { if (userOperation.key == actualOperation.key) {
continue; continue;

@ -1,5 +1,6 @@
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_quill/models/documents/nodes/container.dart';
import '../models/documents/nodes/container.dart';
abstract class RenderContentProxyBox implements RenderBox { abstract class RenderContentProxyBox implements RenderBox {
double getPreferredLineHeight(); double getPreferredLineHeight();

@ -1,19 +1,16 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/documents/document.dart';
import 'package:flutter_quill/models/documents/nodes/embed.dart';
import 'package:flutter_quill/models/documents/style.dart';
import 'package:flutter_quill/models/quill_delta.dart';
import 'package:flutter_quill/utils/diff_delta.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
class QuillController extends ChangeNotifier { import '../models/documents/attribute.dart';
final Document document; import '../models/documents/document.dart';
TextSelection selection; import '../models/documents/nodes/embed.dart';
Style toggledStyle = Style(); import '../models/documents/style.dart';
import '../models/quill_delta.dart';
import '../utils/diff_delta.dart';
class QuillController extends ChangeNotifier {
QuillController({required this.document, required this.selection}); QuillController({required this.document, required this.selection});
factory QuillController.basic() { factory QuillController.basic() {
@ -23,6 +20,10 @@ class QuillController extends ChangeNotifier {
); );
} }
final Document document;
TextSelection selection;
Style toggledStyle = Style();
// item1: Document state before [change]. // item1: Document state before [change].
// //
// item2: Change delta applied to the document. // item2: Change delta applied to the document.
@ -42,7 +43,7 @@ class QuillController extends ChangeNotifier {
} }
void undo() { void undo() {
Tuple2 tup = document.undo(); final tup = document.undo();
if (tup.item1) { if (tup.item1) {
_handleHistoryChange(tup.item2); _handleHistoryChange(tup.item2);
} }
@ -64,7 +65,7 @@ class QuillController extends ChangeNotifier {
} }
void redo() { void redo() {
Tuple2 tup = document.redo(); final tup = document.redo();
if (tup.item1) { if (tup.item1) {
_handleHistoryChange(tup.item2); _handleHistoryChange(tup.item2);
} }
@ -81,7 +82,7 @@ class QuillController extends ChangeNotifier {
Delta? delta; Delta? delta;
if (len > 0 || data is! String || data.isNotEmpty) { if (len > 0 || data is! String || data.isNotEmpty) {
delta = document.replace(index, len, data); delta = document.replace(index, len, data);
bool shouldRetainDelta = toggledStyle.isNotEmpty && var shouldRetainDelta = toggledStyle.isNotEmpty &&
delta.isNotEmpty && delta.isNotEmpty &&
delta.length <= 2 && delta.length <= 2 &&
delta.last.isInsert; delta.last.isInsert;
@ -97,7 +98,7 @@ class QuillController extends ChangeNotifier {
} }
} }
if (shouldRetainDelta) { if (shouldRetainDelta) {
Delta retainDelta = Delta() final retainDelta = Delta()
..retain(index) ..retain(index)
..retain(data is String ? data.length : 1, toggledStyle.toJson()); ..retain(data is String ? data.length : 1, toggledStyle.toJson());
document.compose(retainDelta, ChangeSource.LOCAL); document.compose(retainDelta, ChangeSource.LOCAL);
@ -109,11 +110,11 @@ class QuillController extends ChangeNotifier {
if (delta == null || delta.isEmpty) { if (delta == null || delta.isEmpty) {
_updateSelection(textSelection, ChangeSource.LOCAL); _updateSelection(textSelection, ChangeSource.LOCAL);
} else { } else {
Delta user = Delta() final user = Delta()
..retain(index) ..retain(index)
..insert(data) ..insert(data)
..delete(len); ..delete(len);
int positionDelta = getPositionDelta(user, delta); final positionDelta = getPositionDelta(user, delta);
_updateSelection( _updateSelection(
textSelection.copyWith( textSelection.copyWith(
baseOffset: textSelection.baseOffset + positionDelta, baseOffset: textSelection.baseOffset + positionDelta,
@ -133,8 +134,8 @@ class QuillController extends ChangeNotifier {
toggledStyle = toggledStyle.put(attribute); toggledStyle = toggledStyle.put(attribute);
} }
Delta change = document.format(index, len, attribute); final change = document.format(index, len, attribute);
TextSelection adjustedSelection = selection.copyWith( final adjustedSelection = selection.copyWith(
baseOffset: change.transformPosition(selection.baseOffset), baseOffset: change.transformPosition(selection.baseOffset),
extentOffset: change.transformPosition(selection.extentOffset)); extentOffset: change.transformPosition(selection.extentOffset));
if (selection != adjustedSelection) { if (selection != adjustedSelection) {
@ -176,7 +177,7 @@ class QuillController extends ChangeNotifier {
void _updateSelection(TextSelection textSelection, ChangeSource source) { void _updateSelection(TextSelection textSelection, ChangeSource source) {
selection = textSelection; selection = textSelection;
int end = document.length - 1; final end = document.length - 1;
selection = selection.copyWith( selection = selection.copyWith(
baseOffset: math.min(selection.baseOffset, end), baseOffset: math.min(selection.baseOffset, end),
extentOffset: math.min(selection.extentOffset, end)); extentOffset: math.min(selection.extentOffset, end));

@ -8,15 +8,6 @@ import 'box.dart';
const Duration _FADE_DURATION = Duration(milliseconds: 250); const Duration _FADE_DURATION = Duration(milliseconds: 250);
class CursorStyle { class CursorStyle {
final Color color;
final Color backgroundColor;
final double width;
final double? height;
final Radius? radius;
final Offset? offset;
final bool opacityAnimates;
final bool paintAboveText;
const CursorStyle({ const CursorStyle({
required this.color, required this.color,
required this.backgroundColor, required this.backgroundColor,
@ -28,6 +19,15 @@ class CursorStyle {
this.paintAboveText = false, this.paintAboveText = false,
}); });
final Color color;
final Color backgroundColor;
final double width;
final double? height;
final Radius? radius;
final Offset? offset;
final bool opacityAnimates;
final bool paintAboveText;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
@ -55,20 +55,11 @@ class CursorStyle {
} }
class CursorCont extends ChangeNotifier { class CursorCont extends ChangeNotifier {
final ValueNotifier<bool> show;
final ValueNotifier<bool> _blink;
final ValueNotifier<Color> color;
late AnimationController _blinkOpacityCont;
Timer? _cursorTimer;
bool _targetCursorVisibility = false;
CursorStyle _style;
CursorCont({ CursorCont({
required ValueNotifier<bool> show, required this.show,
required CursorStyle style, required CursorStyle style,
required TickerProvider tickerProvider, required TickerProvider tickerProvider,
}) : show = show, }) : _style = style,
_style = style,
_blink = ValueNotifier(false), _blink = ValueNotifier(false),
color = ValueNotifier(style.color) { color = ValueNotifier(style.color) {
_blinkOpacityCont = _blinkOpacityCont =
@ -76,6 +67,14 @@ class CursorCont extends ChangeNotifier {
_blinkOpacityCont.addListener(_onColorTick); _blinkOpacityCont.addListener(_onColorTick);
} }
final ValueNotifier<bool> show;
final ValueNotifier<bool> _blink;
final ValueNotifier<Color> color;
late AnimationController _blinkOpacityCont;
Timer? _cursorTimer;
bool _targetCursorVisibility = false;
CursorStyle _style;
ValueNotifier<bool> get cursorBlink => _blink; ValueNotifier<bool> get cursorBlink => _blink;
ValueNotifier<Color> get cursorColor => color; ValueNotifier<Color> get cursorColor => color;
@ -99,7 +98,7 @@ class CursorCont extends ChangeNotifier {
void _cursorTick(Timer timer) { void _cursorTick(Timer timer) {
_targetCursorVisibility = !_targetCursorVisibility; _targetCursorVisibility = !_targetCursorVisibility;
double targetOpacity = _targetCursorVisibility ? 1.0 : 0.0; final targetOpacity = _targetCursorVisibility ? 1.0 : 0.0;
if (style.opacityAnimates) { if (style.opacityAnimates) {
_blinkOpacityCont.animateTo(targetOpacity, curve: Curves.easeOut); _blinkOpacityCont.animateTo(targetOpacity, curve: Curves.easeOut);
} else { } else {
@ -133,8 +132,9 @@ class CursorCont extends ChangeNotifier {
_blinkOpacityCont.value = 0.0; _blinkOpacityCont.value = 0.0;
if (style.opacityAnimates) { if (style.opacityAnimates) {
_blinkOpacityCont.stop(); _blinkOpacityCont
_blinkOpacityCont.value = 0.0; ..stop()
..value = 0.0;
} }
} }
@ -156,30 +156,30 @@ class CursorCont extends ChangeNotifier {
} }
class CursorPainter { class CursorPainter {
CursorPainter(this.editable, this.style, this.prototype, this.color,
this.devicePixelRatio);
final RenderContentProxyBox? editable; final RenderContentProxyBox? editable;
final CursorStyle style; final CursorStyle style;
final Rect? prototype; final Rect? prototype;
final Color color; final Color color;
final double devicePixelRatio; final double devicePixelRatio;
CursorPainter(this.editable, this.style, this.prototype, this.color,
this.devicePixelRatio);
void paint(Canvas canvas, Offset offset, TextPosition position) { void paint(Canvas canvas, Offset offset, TextPosition position) {
assert(prototype != null); assert(prototype != null);
Offset caretOffset = final caretOffset =
editable!.getOffsetForCaret(position, prototype) + offset; editable!.getOffsetForCaret(position, prototype) + offset;
Rect caretRect = prototype!.shift(caretOffset); var caretRect = prototype!.shift(caretOffset);
if (style.offset != null) { if (style.offset != null) {
caretRect = caretRect.shift(style.offset!); caretRect = caretRect.shift(style.offset!);
} }
if (caretRect.left < 0.0) { if (caretRect.left < 0.0) {
caretRect = caretRect.shift(Offset(-caretRect.left, 0.0)); caretRect = caretRect.shift(Offset(-caretRect.left, 0));
} }
double? caretHeight = editable!.getFullHeightForCaret(position); final caretHeight = editable!.getFullHeightForCaret(position);
if (caretHeight != null) { if (caretHeight != null) {
switch (defaultTargetPlatform) { switch (defaultTargetPlatform) {
case TargetPlatform.android: case TargetPlatform.android:
@ -207,8 +207,8 @@ class CursorPainter {
} }
} }
Offset caretPosition = editable!.localToGlobal(caretRect.topLeft); final caretPosition = editable!.localToGlobal(caretRect.topLeft);
double pixelMultiple = 1.0 / devicePixelRatio; final pixelMultiple = 1.0 / devicePixelRatio;
caretRect = caretRect.shift(Offset( caretRect = caretRect.shift(Offset(
caretPosition.dx.isFinite caretPosition.dx.isFinite
? (caretPosition.dx / pixelMultiple).round() * pixelMultiple - ? (caretPosition.dx / pixelMultiple).round() * pixelMultiple -
@ -219,13 +219,13 @@ class CursorPainter {
caretPosition.dy caretPosition.dy
: caretPosition.dy)); : caretPosition.dy));
Paint paint = Paint()..color = color; final paint = Paint()..color = color;
if (style.radius == null) { if (style.radius == null) {
canvas.drawRect(caretRect, paint); canvas.drawRect(caretRect, paint);
return; return;
} }
RRect caretRRect = RRect.fromRectAndRadius(caretRect, style.radius!); final caretRRect = RRect.fromRectAndRadius(caretRect, style.radius!);
canvas.drawRRect(caretRRect, paint); canvas.drawRRect(caretRRect, paint);
} }
} }

@ -3,21 +3,21 @@ import 'package:flutter/widgets.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
class QuillStyles extends InheritedWidget { class QuillStyles extends InheritedWidget {
final DefaultStyles data;
const QuillStyles({ const QuillStyles({
required this.data, required this.data,
required Widget child, required Widget child,
Key? key, Key? key,
}) : super(key: key, child: child); }) : super(key: key, child: child);
final DefaultStyles data;
@override @override
bool updateShouldNotify(QuillStyles oldWidget) { bool updateShouldNotify(QuillStyles oldWidget) {
return data != oldWidget.data; return data != oldWidget.data;
} }
static DefaultStyles? getStyles(BuildContext context, bool nullOk) { static DefaultStyles? getStyles(BuildContext context, bool nullOk) {
var widget = context.dependOnInheritedWidgetOfExactType<QuillStyles>(); final widget = context.dependOnInheritedWidgetOfExactType<QuillStyles>();
if (widget == null && nullOk) { if (widget == null && nullOk) {
return null; return null;
} }
@ -27,6 +27,13 @@ class QuillStyles extends InheritedWidget {
} }
class DefaultTextBlockStyle { class DefaultTextBlockStyle {
DefaultTextBlockStyle(
this.style,
this.verticalSpacing,
this.lineSpacing,
this.decoration,
);
final TextStyle style; final TextStyle style;
final Tuple2<double, double> verticalSpacing; final Tuple2<double, double> verticalSpacing;
@ -34,12 +41,32 @@ class DefaultTextBlockStyle {
final Tuple2<double, double> lineSpacing; final Tuple2<double, double> lineSpacing;
final BoxDecoration? decoration; final BoxDecoration? decoration;
DefaultTextBlockStyle(
this.style, this.verticalSpacing, this.lineSpacing, this.decoration);
} }
class DefaultStyles { class DefaultStyles {
DefaultStyles({
this.h1,
this.h2,
this.h3,
this.paragraph,
this.bold,
this.italic,
this.underline,
this.strikeThrough,
this.link,
this.color,
this.placeHolder,
this.lists,
this.quote,
this.code,
this.indent,
this.align,
this.leading,
this.sizeSmall,
this.sizeLarge,
this.sizeHuge,
});
final DefaultTextBlockStyle? h1; final DefaultTextBlockStyle? h1;
final DefaultTextBlockStyle? h2; final DefaultTextBlockStyle? h2;
final DefaultTextBlockStyle? h3; final DefaultTextBlockStyle? h3;
@ -61,36 +88,14 @@ class DefaultStyles {
final DefaultTextBlockStyle? align; final DefaultTextBlockStyle? align;
final DefaultTextBlockStyle? leading; final DefaultTextBlockStyle? leading;
DefaultStyles(
{this.h1,
this.h2,
this.h3,
this.paragraph,
this.bold,
this.italic,
this.underline,
this.strikeThrough,
this.link,
this.color,
this.placeHolder,
this.lists,
this.quote,
this.code,
this.indent,
this.align,
this.leading,
this.sizeSmall,
this.sizeLarge,
this.sizeHuge});
static DefaultStyles getInstance(BuildContext context) { static DefaultStyles getInstance(BuildContext context) {
ThemeData themeData = Theme.of(context); final themeData = Theme.of(context);
DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context); final defaultTextStyle = DefaultTextStyle.of(context);
TextStyle baseStyle = defaultTextStyle.style.copyWith( final baseStyle = defaultTextStyle.style.copyWith(
fontSize: 16.0, fontSize: 16,
height: 1.3, height: 1.3,
); );
Tuple2<double, double> baseSpacing = const Tuple2(6.0, 0); const baseSpacing = Tuple2<double, double>(6, 0);
String fontFamily; String fontFamily;
switch (themeData.platform) { switch (themeData.platform) {
case TargetPlatform.iOS: case TargetPlatform.iOS:
@ -110,36 +115,36 @@ class DefaultStyles {
return DefaultStyles( return DefaultStyles(
h1: DefaultTextBlockStyle( h1: DefaultTextBlockStyle(
defaultTextStyle.style.copyWith( defaultTextStyle.style.copyWith(
fontSize: 34.0, fontSize: 34,
color: defaultTextStyle.style.color!.withOpacity(0.70), color: defaultTextStyle.style.color!.withOpacity(0.70),
height: 1.15, height: 1.15,
fontWeight: FontWeight.w300, fontWeight: FontWeight.w300,
), ),
const Tuple2(16.0, 0.0), const Tuple2(16, 0),
const Tuple2(0.0, 0.0), const Tuple2(0, 0),
null), null),
h2: DefaultTextBlockStyle( h2: DefaultTextBlockStyle(
defaultTextStyle.style.copyWith( defaultTextStyle.style.copyWith(
fontSize: 24.0, fontSize: 24,
color: defaultTextStyle.style.color!.withOpacity(0.70), color: defaultTextStyle.style.color!.withOpacity(0.70),
height: 1.15, height: 1.15,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
), ),
const Tuple2(8.0, 0.0), const Tuple2(8, 0),
const Tuple2(0.0, 0.0), const Tuple2(0, 0),
null), null),
h3: DefaultTextBlockStyle( h3: DefaultTextBlockStyle(
defaultTextStyle.style.copyWith( defaultTextStyle.style.copyWith(
fontSize: 20.0, fontSize: 20,
color: defaultTextStyle.style.color!.withOpacity(0.70), color: defaultTextStyle.style.color!.withOpacity(0.70),
height: 1.25, height: 1.25,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
const Tuple2(8.0, 0.0), const Tuple2(8, 0),
const Tuple2(0.0, 0.0), const Tuple2(0, 0),
null), null),
paragraph: DefaultTextBlockStyle( paragraph: DefaultTextBlockStyle(
baseStyle, const Tuple2(0.0, 0.0), const Tuple2(0.0, 0.0), null), baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null),
bold: const TextStyle(fontWeight: FontWeight.bold), bold: const TextStyle(fontWeight: FontWeight.bold),
italic: const TextStyle(fontStyle: FontStyle.italic), italic: const TextStyle(fontStyle: FontStyle.italic),
underline: const TextStyle(decoration: TextDecoration.underline), underline: const TextStyle(decoration: TextDecoration.underline),
@ -150,19 +155,19 @@ class DefaultStyles {
), ),
placeHolder: DefaultTextBlockStyle( placeHolder: DefaultTextBlockStyle(
defaultTextStyle.style.copyWith( defaultTextStyle.style.copyWith(
fontSize: 20.0, fontSize: 20,
height: 1.5, height: 1.5,
color: Colors.grey.withOpacity(0.6), color: Colors.grey.withOpacity(0.6),
), ),
const Tuple2(0.0, 0.0), const Tuple2(0, 0),
const Tuple2(0.0, 0.0), const Tuple2(0, 0),
null), null),
lists: DefaultTextBlockStyle( lists: DefaultTextBlockStyle(
baseStyle, baseSpacing, const Tuple2(0.0, 6.0), null), baseStyle, baseSpacing, const Tuple2(0, 6), null),
quote: DefaultTextBlockStyle( quote: DefaultTextBlockStyle(
TextStyle(color: baseStyle.color!.withOpacity(0.6)), TextStyle(color: baseStyle.color!.withOpacity(0.6)),
baseSpacing, baseSpacing,
const Tuple2(6.0, 2.0), const Tuple2(6, 2),
BoxDecoration( BoxDecoration(
border: Border( border: Border(
left: BorderSide(width: 4, color: Colors.grey.shade300), left: BorderSide(width: 4, color: Colors.grey.shade300),
@ -172,24 +177,24 @@ class DefaultStyles {
TextStyle( TextStyle(
color: Colors.blue.shade900.withOpacity(0.9), color: Colors.blue.shade900.withOpacity(0.9),
fontFamily: fontFamily, fontFamily: fontFamily,
fontSize: 13.0, fontSize: 13,
height: 1.15, height: 1.15,
), ),
baseSpacing, baseSpacing,
const Tuple2(0.0, 0.0), const Tuple2(0, 0),
BoxDecoration( BoxDecoration(
color: Colors.grey.shade50, color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(2),
)), )),
indent: DefaultTextBlockStyle( indent: DefaultTextBlockStyle(
baseStyle, baseSpacing, const Tuple2(0.0, 6.0), null), baseStyle, baseSpacing, const Tuple2(0, 6), null),
align: DefaultTextBlockStyle( align: DefaultTextBlockStyle(
baseStyle, const Tuple2(0.0, 0.0), const Tuple2(0.0, 0.0), null), baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null),
leading: DefaultTextBlockStyle( leading: DefaultTextBlockStyle(
baseStyle, const Tuple2(0.0, 0.0), const Tuple2(0.0, 0.0), null), baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null),
sizeSmall: const TextStyle(fontSize: 10.0), sizeSmall: const TextStyle(fontSize: 10),
sizeLarge: const TextStyle(fontSize: 18.0), sizeLarge: const TextStyle(fontSize: 18),
sizeHuge: const TextStyle(fontSize: 22.0)); sizeHuge: const TextStyle(fontSize: 22));
} }
DefaultStyles merge(DefaultStyles other) { DefaultStyles merge(DefaultStyles other) {

@ -2,10 +2,10 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_quill/models/documents/nodes/leaf.dart';
import 'package:flutter_quill/widgets/text_selection.dart';
import '../models/documents/nodes/leaf.dart';
import 'editor.dart'; import 'editor.dart';
import 'text_selection.dart';
typedef EmbedBuilder = Widget Function(BuildContext context, Embed node); typedef EmbedBuilder = Widget Function(BuildContext context, Embed node);
@ -18,11 +18,11 @@ abstract class EditorTextSelectionGestureDetectorBuilderDelegate {
} }
class EditorTextSelectionGestureDetectorBuilder { class EditorTextSelectionGestureDetectorBuilder {
EditorTextSelectionGestureDetectorBuilder(this.delegate);
final EditorTextSelectionGestureDetectorBuilderDelegate delegate; final EditorTextSelectionGestureDetectorBuilderDelegate delegate;
bool shouldShowSelectionToolbar = true; bool shouldShowSelectionToolbar = true;
EditorTextSelectionGestureDetectorBuilder(this.delegate);
EditorState? getEditor() { EditorState? getEditor() {
return delegate.getEditableTextKey().currentState; return delegate.getEditableTextKey().currentState;
} }
@ -34,7 +34,7 @@ class EditorTextSelectionGestureDetectorBuilder {
void onTapDown(TapDownDetails details) { void onTapDown(TapDownDetails details) {
getRenderEditor()!.handleTapDown(details); getRenderEditor()!.handleTapDown(details);
PointerDeviceKind? kind = details.kind; final kind = details.kind;
shouldShowSelectionToolbar = kind == null || shouldShowSelectionToolbar = kind == null ||
kind == PointerDeviceKind.touch || kind == PointerDeviceKind.touch ||
kind == PointerDeviceKind.stylus; kind == PointerDeviceKind.stylus;

@ -8,25 +8,23 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/documents/document.dart';
import 'package:flutter_quill/models/documents/nodes/container.dart'
as container_node;
import 'package:flutter_quill/models/documents/nodes/embed.dart';
import 'package:flutter_quill/models/documents/nodes/leaf.dart' as leaf;
import 'package:flutter_quill/models/documents/nodes/line.dart';
import 'package:flutter_quill/models/documents/nodes/node.dart';
import 'package:flutter_quill/widgets/image.dart';
import 'package:flutter_quill/widgets/raw_editor.dart';
import 'package:flutter_quill/widgets/text_selection.dart';
import 'package:string_validator/string_validator.dart'; import 'package:string_validator/string_validator.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../models/documents/attribute.dart';
import '../models/documents/document.dart';
import '../models/documents/nodes/container.dart' as container_node;
import '../models/documents/nodes/embed.dart';
import '../models/documents/nodes/leaf.dart' as leaf;
import '../models/documents/nodes/line.dart';
import 'box.dart'; import 'box.dart';
import 'controller.dart'; import 'controller.dart';
import 'cursor.dart'; import 'cursor.dart';
import 'default_styles.dart'; import 'default_styles.dart';
import 'delegate.dart'; import 'delegate.dart';
import 'image.dart';
import 'raw_editor.dart';
import 'text_selection.dart';
const linkPrefixes = [ const linkPrefixes = [
'mailto:', // email 'mailto:', // email
@ -101,7 +99,7 @@ Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) {
assert(!kIsWeb, 'Please provide EmbedBuilder for Web'); assert(!kIsWeb, 'Please provide EmbedBuilder for Web');
switch (node.value.type) { switch (node.value.type) {
case 'image': case 'image':
String imageUrl = _standardizeImageUrl(node.value.data); final imageUrl = _standardizeImageUrl(node.value.data);
return imageUrl.startsWith('http') return imageUrl.startsWith('http')
? Image.network(imageUrl) ? Image.network(imageUrl)
: isBase64(imageUrl) : isBase64(imageUrl)
@ -116,6 +114,43 @@ Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) {
} }
class QuillEditor extends StatefulWidget { class QuillEditor extends StatefulWidget {
const QuillEditor({
required this.controller,
required this.focusNode,
required this.scrollController,
required this.scrollable,
required this.padding,
required this.autoFocus,
required this.readOnly,
required this.expands,
this.showCursor,
this.placeholder,
this.enableInteractiveSelection = true,
this.minHeight,
this.maxHeight,
this.customStyles,
this.textCapitalization = TextCapitalization.sentences,
this.keyboardAppearance = Brightness.light,
this.scrollPhysics,
this.onLaunchUrl,
this.embedBuilder = _defaultEmbedBuilder,
});
factory QuillEditor.basic({
required QuillController controller,
required bool readOnly,
}) {
return QuillEditor(
controller: controller,
scrollController: ScrollController(),
scrollable: true,
focusNode: FocusNode(),
autoFocus: true,
readOnly: readOnly,
expands: false,
padding: EdgeInsets.zero);
}
final QuillController controller; final QuillController controller;
final FocusNode focusNode; final FocusNode focusNode;
final ScrollController scrollController; final ScrollController scrollController;
@ -146,45 +181,6 @@ class QuillEditor extends StatefulWidget {
final bool Function(LongPressEndDetails details, TextPosition textPosition)? onSingleLongTapEnd; final bool Function(LongPressEndDetails details, TextPosition textPosition)? onSingleLongTapEnd;
final EmbedBuilder embedBuilder; final EmbedBuilder embedBuilder;
const QuillEditor(
{required this.controller,
required this.focusNode,
required this.scrollController,
required this.scrollable,
required this.padding,
required this.autoFocus,
required this.readOnly,
required this.expands,
this.showCursor,
this.placeholder,
this.enableInteractiveSelection = true,
this.minHeight,
this.maxHeight,
this.customStyles,
this.textCapitalization = TextCapitalization.sentences,
this.keyboardAppearance = Brightness.light,
this.scrollPhysics,
this.onLaunchUrl,
this.onTapDown,
this.onTapUp,
this.onSingleLongTapStart,
this.onSingleLongTapMoveUpdate,
this.onSingleLongTapEnd,
this.embedBuilder = _defaultEmbedBuilder});
factory QuillEditor.basic(
{required QuillController controller, required bool readOnly}) {
return QuillEditor(
controller: controller,
scrollController: ScrollController(),
scrollable: true,
focusNode: FocusNode(),
autoFocus: true,
readOnly: readOnly,
expands: false,
padding: EdgeInsets.zero);
}
@override @override
_QuillEditorState createState() => _QuillEditorState(); _QuillEditorState createState() => _QuillEditorState();
} }
@ -204,8 +200,8 @@ class _QuillEditorState extends State<QuillEditor>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ThemeData theme = Theme.of(context); final theme = Theme.of(context);
TextSelectionThemeData selectionTheme = TextSelectionTheme.of(context); final selectionTheme = TextSelectionTheme.of(context);
TextSelectionControls textSelectionControls; TextSelectionControls textSelectionControls;
bool paintCursorAboveText; bool paintCursorAboveText;
@ -229,7 +225,7 @@ class _QuillEditorState extends State<QuillEditor>
break; break;
case TargetPlatform.iOS: case TargetPlatform.iOS:
case TargetPlatform.macOS: case TargetPlatform.macOS:
CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); final cupertinoTheme = CupertinoTheme.of(context);
textSelectionControls = cupertinoTextSelectionControls; textSelectionControls = cupertinoTextSelectionControls;
paintCursorAboveText = true; paintCursorAboveText = true;
cursorOpacityAnimates = true; cursorOpacityAnimates = true;
@ -237,7 +233,7 @@ class _QuillEditorState extends State<QuillEditor>
selectionTheme.cursorColor ?? cupertinoTheme.primaryColor; selectionTheme.cursorColor ?? cupertinoTheme.primaryColor;
selectionColor = selectionTheme.selectionColor ?? selectionColor = selectionTheme.selectionColor ??
cupertinoTheme.primaryColor.withOpacity(0.40); cupertinoTheme.primaryColor.withOpacity(0.40);
cursorRadius ??= const Radius.circular(2.0); cursorRadius ??= const Radius.circular(2);
cursorOffset = Offset( cursorOffset = Offset(
iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
break; break;
@ -269,7 +265,7 @@ class _QuillEditorState extends State<QuillEditor>
CursorStyle( CursorStyle(
color: cursorColor, color: cursorColor,
backgroundColor: Colors.grey, backgroundColor: Colors.grey,
width: 2.0, width: 2,
radius: cursorRadius, radius: cursorRadius,
offset: cursorOffset, offset: cursorOffset,
paintAboveText: paintCursorAboveText, paintAboveText: paintCursorAboveText,
@ -286,6 +282,11 @@ class _QuillEditorState extends State<QuillEditor>
widget.keyboardAppearance, widget.keyboardAppearance,
widget.enableInteractiveSelection, widget.enableInteractiveSelection,
widget.scrollPhysics, widget.scrollPhysics,
widget.onTapDown,
widget.onTapUp,
widget.onSingleLongTapStart,
widget.onSingleLongTapMoveUpdate,
widget.onSingleLongTapEnd,
widget.embedBuilder), widget.embedBuilder),
); );
} }
@ -312,10 +313,10 @@ class _QuillEditorState extends State<QuillEditor>
class _QuillEditorSelectionGestureDetectorBuilder class _QuillEditorSelectionGestureDetectorBuilder
extends EditorTextSelectionGestureDetectorBuilder { extends EditorTextSelectionGestureDetectorBuilder {
final _QuillEditorState _state;
_QuillEditorSelectionGestureDetectorBuilder(this._state) : super(_state); _QuillEditorSelectionGestureDetectorBuilder(this._state) : super(_state);
final _QuillEditorState _state;
@override @override
void onForcePressStart(ForcePressDetails details) { void onForcePressStart(ForcePressDetails details) {
super.onForcePressStart(details); super.onForcePressStart(details);
@ -369,16 +370,14 @@ class _QuillEditorSelectionGestureDetectorBuilder
if (_state.widget.controller.document.isEmpty()) { if (_state.widget.controller.document.isEmpty()) {
return false; return false;
} }
TextPosition pos = final pos = getRenderEditor()!.getPositionForOffset(details.globalPosition);
getRenderEditor()!.getPositionForOffset(details.globalPosition); final result =
container_node.ChildQuery result =
getEditor()!.widget.controller.document.queryChild(pos.offset); getEditor()!.widget.controller.document.queryChild(pos.offset);
if (result.node == null) { if (result.node == null) {
return false; return false;
} }
Line line = result.node as Line; final line = result.node as Line;
container_node.ChildQuery segmentResult = final segmentResult = line.queryChild(result.offset, false);
line.queryChild(result.offset, false);
if (segmentResult.node == null) { if (segmentResult.node == null) {
if (line.length == 1) { if (line.length == 1) {
// tapping when no text yet on this line // tapping when no text yet on this line
@ -389,7 +388,7 @@ class _QuillEditorSelectionGestureDetectorBuilder
} }
return false; return false;
} }
leaf.Leaf segment = segmentResult.node as leaf.Leaf; final segment = segmentResult.node as leaf.Leaf;
if (segment.style.containsKey(Attribute.link.key)) { if (segment.style.containsKey(Attribute.link.key)) {
var launchUrl = getEditor()!.widget.onLaunchUrl; var launchUrl = getEditor()!.widget.onLaunchUrl;
launchUrl ??= _launchUrl; launchUrl ??= _launchUrl;
@ -405,9 +404,9 @@ class _QuillEditorSelectionGestureDetectorBuilder
return false; return false;
} }
if (getEditor()!.widget.readOnly && segment.value is BlockEmbed) { if (getEditor()!.widget.readOnly && segment.value is BlockEmbed) {
BlockEmbed blockEmbed = segment.value as BlockEmbed; final blockEmbed = segment.value as BlockEmbed;
if (blockEmbed.type == 'image') { if (blockEmbed.type == 'image') {
final String imageUrl = _standardizeImageUrl(blockEmbed.data); final imageUrl = _standardizeImageUrl(blockEmbed.data);
Navigator.push( Navigator.push(
getEditor()!.context, getEditor()!.context,
MaterialPageRoute( MaterialPageRoute(
@ -438,7 +437,7 @@ class _QuillEditorSelectionGestureDetectorBuilder
return false; return false;
} }
// segmentResult.offset == 0 means tap at the beginning of the TextLine // segmentResult.offset == 0 means tap at the beginning of the TextLine
String? listVal = line.style.attributes[Attribute.list.key]!.value; final String? listVal = line.style.attributes[Attribute.list.key]!.value;
if (listVal == Attribute.unchecked.value) { if (listVal == Attribute.unchecked.value) {
getEditor()! getEditor()!
.widget .widget
@ -455,7 +454,7 @@ class _QuillEditorSelectionGestureDetectorBuilder
return true; return true;
} }
void _launchUrl(String url) async { Future<void> _launchUrl(String url) async {
await launch(url); await launch(url);
} }
@ -485,7 +484,7 @@ class _QuillEditorSelectionGestureDetectorBuilder
getEditor()!.hideToolbar(); getEditor()!.hideToolbar();
bool positionSelected = _onTapping(details); final positionSelected = _onTapping(details);
if (delegate.getSelectionEnabled() && !positionSelected) { if (delegate.getSelectionEnabled() && !positionSelected) {
switch (Theme.of(_state.context).platform) { switch (Theme.of(_state.context).platform) {
@ -567,6 +566,24 @@ typedef TextSelectionChangedHandler = void Function(
class RenderEditor extends RenderEditableContainerBox class RenderEditor extends RenderEditableContainerBox
implements RenderAbstractEditor { implements RenderAbstractEditor {
RenderEditor(
List<RenderEditableBox>? children,
TextDirection textDirection,
EdgeInsetsGeometry padding,
this.document,
this.selection,
this._hasFocus,
this.onSelectionChanged,
this._startHandleLayerLink,
this._endHandleLayerLink,
EdgeInsets floatingCursorAddedMargin,
) : super(
children,
document.root,
textDirection,
padding,
);
Document document; Document document;
TextSelection selection; TextSelection selection;
bool _hasFocus = false; bool _hasFocus = false;
@ -582,24 +599,6 @@ class RenderEditor extends RenderEditableContainerBox
ValueListenable<bool> get selectionEndInViewport => _selectionEndInViewport; ValueListenable<bool> get selectionEndInViewport => _selectionEndInViewport;
final ValueNotifier<bool> _selectionEndInViewport = ValueNotifier<bool>(true); final ValueNotifier<bool> _selectionEndInViewport = ValueNotifier<bool>(true);
RenderEditor(
List<RenderEditableBox>? children,
TextDirection textDirection,
EdgeInsetsGeometry padding,
this.document,
this.selection,
this._hasFocus,
this.onSelectionChanged,
this._startHandleLayerLink,
this._endHandleLayerLink,
EdgeInsets floatingCursorAddedMargin)
: super(
children,
document.root,
textDirection,
padding,
);
void setDocument(Document doc) { void setDocument(Document doc) {
if (document == doc) { if (document == doc) {
return; return;
@ -644,22 +643,21 @@ class RenderEditor extends RenderEditableContainerBox
List<TextSelectionPoint> getEndpointsForSelection( List<TextSelectionPoint> getEndpointsForSelection(
TextSelection textSelection) { TextSelection textSelection) {
if (textSelection.isCollapsed) { if (textSelection.isCollapsed) {
RenderEditableBox child = childAtPosition(textSelection.extent); final child = childAtPosition(textSelection.extent);
TextPosition localPosition = TextPosition( final localPosition = TextPosition(
offset: offset: textSelection.extentOffset - child.getContainer().offset);
textSelection.extentOffset - child.getContainer().getOffset()); final localOffset = child.getOffsetForCaret(localPosition);
Offset localOffset = child.getOffsetForCaret(localPosition); final parentData = child.parentData as BoxParentData;
BoxParentData parentData = child.parentData as BoxParentData;
return <TextSelectionPoint>[ return <TextSelectionPoint>[
TextSelectionPoint( TextSelectionPoint(
Offset(0.0, child.preferredLineHeight(localPosition)) + Offset(0, child.preferredLineHeight(localPosition)) +
localOffset + localOffset +
parentData.offset, parentData.offset,
null) null)
]; ];
} }
Node? baseNode = _container.queryChild(textSelection.start, false).node; final baseNode = _container.queryChild(textSelection.start, false).node;
var baseChild = firstChild; var baseChild = firstChild;
while (baseChild != null) { while (baseChild != null) {
@ -670,15 +668,14 @@ class RenderEditor extends RenderEditableContainerBox
} }
assert(baseChild != null); assert(baseChild != null);
BoxParentData baseParentData = baseChild!.parentData as BoxParentData; final baseParentData = baseChild!.parentData as BoxParentData;
TextSelection baseSelection = final baseSelection =
localSelection(baseChild.getContainer(), textSelection, true); localSelection(baseChild.getContainer(), textSelection, true);
TextSelectionPoint basePoint = var basePoint = baseChild.getBaseEndpointForSelection(baseSelection);
baseChild.getBaseEndpointForSelection(baseSelection);
basePoint = TextSelectionPoint( basePoint = TextSelectionPoint(
basePoint.point + baseParentData.offset, basePoint.direction); basePoint.point + baseParentData.offset, basePoint.direction);
Node? extentNode = _container.queryChild(textSelection.end, false).node; final extentNode = _container.queryChild(textSelection.end, false).node;
RenderEditableBox? extentChild = baseChild; RenderEditableBox? extentChild = baseChild;
while (extentChild != null) { while (extentChild != null) {
if (extentChild.getContainer() == extentNode) { if (extentChild.getContainer() == extentNode) {
@ -688,10 +685,10 @@ class RenderEditor extends RenderEditableContainerBox
} }
assert(extentChild != null); assert(extentChild != null);
BoxParentData extentParentData = extentChild!.parentData as BoxParentData; final extentParentData = extentChild!.parentData as BoxParentData;
TextSelection extentSelection = final extentSelection =
localSelection(extentChild.getContainer(), textSelection, true); localSelection(extentChild.getContainer(), textSelection, true);
TextSelectionPoint extentPoint = var extentPoint =
extentChild.getExtentEndpointForSelection(extentSelection); extentChild.getExtentEndpointForSelection(extentSelection);
extentPoint = TextSelectionPoint( extentPoint = TextSelectionPoint(
extentPoint.point + extentParentData.offset, extentPoint.direction); extentPoint.point + extentParentData.offset, extentPoint.direction);
@ -712,9 +709,9 @@ class RenderEditor extends RenderEditableContainerBox
Offset? to, Offset? to,
SelectionChangedCause cause, SelectionChangedCause cause,
) { ) {
TextPosition firstPosition = getPositionForOffset(from); final firstPosition = getPositionForOffset(from);
TextSelection firstWord = selectWordAtPosition(firstPosition); final firstWord = selectWordAtPosition(firstPosition);
TextSelection lastWord = final lastWord =
to == null ? firstWord : selectWordAtPosition(getPositionForOffset(to)); to == null ? firstWord : selectWordAtPosition(getPositionForOffset(to));
_handleSelectionChange( _handleSelectionChange(
@ -731,7 +728,7 @@ class RenderEditor extends RenderEditableContainerBox
TextSelection nextSelection, TextSelection nextSelection,
SelectionChangedCause cause, SelectionChangedCause cause,
) { ) {
bool focusingEmpty = nextSelection.baseOffset == 0 && final focusingEmpty = nextSelection.baseOffset == 0 &&
nextSelection.extentOffset == 0 && nextSelection.extentOffset == 0 &&
!_hasFocus; !_hasFocus;
if (nextSelection == selection && if (nextSelection == selection &&
@ -745,15 +742,15 @@ class RenderEditor extends RenderEditableContainerBox
@override @override
void selectWordEdge(SelectionChangedCause cause) { void selectWordEdge(SelectionChangedCause cause) {
assert(_lastTapDownPosition != null); assert(_lastTapDownPosition != null);
TextPosition position = getPositionForOffset(_lastTapDownPosition!); final position = getPositionForOffset(_lastTapDownPosition!);
RenderEditableBox child = childAtPosition(position); final child = childAtPosition(position);
int nodeOffset = child.getContainer().getOffset(); final nodeOffset = child.getContainer().offset;
TextPosition localPosition = TextPosition( final localPosition = TextPosition(
offset: position.offset - nodeOffset, offset: position.offset - nodeOffset,
affinity: position.affinity, affinity: position.affinity,
); );
TextRange localWord = child.getWordBoundary(localPosition); final localWord = child.getWordBoundary(localPosition);
TextRange word = TextRange( final word = TextRange(
start: localWord.start + nodeOffset, start: localWord.start + nodeOffset,
end: localWord.end + nodeOffset, end: localWord.end + nodeOffset,
); );
@ -777,17 +774,17 @@ class RenderEditor extends RenderEditableContainerBox
Offset? to, Offset? to,
SelectionChangedCause cause, SelectionChangedCause cause,
) { ) {
TextPosition fromPosition = getPositionForOffset(from); final fromPosition = getPositionForOffset(from);
TextPosition? toPosition = to == null ? null : getPositionForOffset(to); final toPosition = to == null ? null : getPositionForOffset(to);
int baseOffset = fromPosition.offset; var baseOffset = fromPosition.offset;
int extentOffset = fromPosition.offset; var extentOffset = fromPosition.offset;
if (toPosition != null) { if (toPosition != null) {
baseOffset = math.min(fromPosition.offset, toPosition.offset); baseOffset = math.min(fromPosition.offset, toPosition.offset);
extentOffset = math.max(fromPosition.offset, toPosition.offset); extentOffset = math.max(fromPosition.offset, toPosition.offset);
} }
TextSelection newSelection = TextSelection( final newSelection = TextSelection(
baseOffset: baseOffset, baseOffset: baseOffset,
extentOffset: extentOffset, extentOffset: extentOffset,
affinity: fromPosition.affinity, affinity: fromPosition.affinity,
@ -807,12 +804,12 @@ class RenderEditor extends RenderEditableContainerBox
@override @override
TextSelection selectWordAtPosition(TextPosition position) { TextSelection selectWordAtPosition(TextPosition position) {
RenderEditableBox child = childAtPosition(position); final child = childAtPosition(position);
int nodeOffset = child.getContainer().getOffset(); final nodeOffset = child.getContainer().offset;
TextPosition localPosition = TextPosition( final localPosition = TextPosition(
offset: position.offset - nodeOffset, affinity: position.affinity); offset: position.offset - nodeOffset, affinity: position.affinity);
TextRange localWord = child.getWordBoundary(localPosition); final localWord = child.getWordBoundary(localPosition);
TextRange word = TextRange( final word = TextRange(
start: localWord.start + nodeOffset, start: localWord.start + nodeOffset,
end: localWord.end + nodeOffset, end: localWord.end + nodeOffset,
); );
@ -824,12 +821,12 @@ class RenderEditor extends RenderEditableContainerBox
@override @override
TextSelection selectLineAtPosition(TextPosition position) { TextSelection selectLineAtPosition(TextPosition position) {
RenderEditableBox child = childAtPosition(position); final child = childAtPosition(position);
int nodeOffset = child.getContainer().getOffset(); final nodeOffset = child.getContainer().offset;
TextPosition localPosition = TextPosition( final localPosition = TextPosition(
offset: position.offset - nodeOffset, affinity: position.affinity); offset: position.offset - nodeOffset, affinity: position.affinity);
TextRange localLineRange = child.getLineBoundary(localPosition); final localLineRange = child.getLineBoundary(localPosition);
TextRange line = TextRange( final line = TextRange(
start: localLineRange.start + nodeOffset, start: localLineRange.start + nodeOffset,
end: localLineRange.end + nodeOffset, end: localLineRange.end + nodeOffset,
); );
@ -879,21 +876,21 @@ class RenderEditor extends RenderEditableContainerBox
@override @override
double preferredLineHeight(TextPosition position) { double preferredLineHeight(TextPosition position) {
RenderEditableBox child = childAtPosition(position); final child = childAtPosition(position);
return child.preferredLineHeight(TextPosition( return child.preferredLineHeight(
offset: position.offset - child.getContainer().getOffset())); TextPosition(offset: position.offset - child.getContainer().offset));
} }
@override @override
TextPosition getPositionForOffset(Offset offset) { TextPosition getPositionForOffset(Offset offset) {
Offset local = globalToLocal(offset); final local = globalToLocal(offset);
RenderEditableBox child = childAtOffset(local)!; final child = childAtOffset(local)!;
BoxParentData parentData = child.parentData as BoxParentData; final parentData = child.parentData as BoxParentData;
Offset localOffset = local - parentData.offset; final localOffset = local - parentData.offset;
TextPosition localPosition = child.getPositionForOffset(localOffset); final localPosition = child.getPositionForOffset(localOffset);
return TextPosition( return TextPosition(
offset: localPosition.offset + child.getContainer().getOffset(), offset: localPosition.offset + child.getContainer().offset,
affinity: localPosition.affinity, affinity: localPosition.affinity,
); );
} }
@ -905,15 +902,14 @@ class RenderEditor extends RenderEditableContainerBox
/// Returns null if [selection] is already visible. /// Returns null if [selection] is already visible.
double? getOffsetToRevealCursor( double? getOffsetToRevealCursor(
double viewportHeight, double scrollOffset, double offsetInViewport) { double viewportHeight, double scrollOffset, double offsetInViewport) {
List<TextSelectionPoint> endpoints = getEndpointsForSelection(selection); final endpoints = getEndpointsForSelection(selection);
TextSelectionPoint endpoint = endpoints.first; final endpoint = endpoints.first;
RenderEditableBox child = childAtPosition(selection.extent); final child = childAtPosition(selection.extent);
const kMargin = 8.0; const kMargin = 8.0;
double caretTop = endpoint.point.dy - final caretTop = endpoint.point.dy -
child.preferredLineHeight(TextPosition( child.preferredLineHeight(TextPosition(
offset: offset: selection.extentOffset - child.getContainer().offset)) -
selection.extentOffset - child.getContainer().getOffset())) -
kMargin + kMargin +
offsetInViewport; offsetInViewport;
final caretBottom = endpoint.point.dy + kMargin + offsetInViewport; final caretBottom = endpoint.point.dy + kMargin + offsetInViewport;
@ -926,7 +922,7 @@ class RenderEditor extends RenderEditableContainerBox
if (dy == null) { if (dy == null) {
return null; return null;
} }
return math.max(dy, 0.0); return math.max(dy, 0);
} }
} }
@ -939,17 +935,20 @@ class RenderEditableContainerBox extends RenderBox
EditableContainerParentData>, EditableContainerParentData>,
RenderBoxContainerDefaultsMixin<RenderEditableBox, RenderBoxContainerDefaultsMixin<RenderEditableBox,
EditableContainerParentData> { EditableContainerParentData> {
RenderEditableContainerBox(
List<RenderEditableBox>? children,
this._container,
this.textDirection,
this._padding,
) : assert(_padding.isNonNegative) {
addAll(children);
}
container_node.Container _container; container_node.Container _container;
TextDirection textDirection; TextDirection textDirection;
EdgeInsetsGeometry _padding; EdgeInsetsGeometry _padding;
EdgeInsets? _resolvedPadding; EdgeInsets? _resolvedPadding;
RenderEditableContainerBox(List<RenderEditableBox>? children, this._container,
this.textDirection, this._padding)
: assert(_padding.isNonNegative) {
addAll(children);
}
container_node.Container getContainer() { container_node.Container getContainer() {
return _container; return _container;
} }
@ -988,7 +987,7 @@ class RenderEditableContainerBox extends RenderBox
RenderEditableBox childAtPosition(TextPosition position) { RenderEditableBox childAtPosition(TextPosition position) {
assert(firstChild != null); assert(firstChild != null);
Node? targetNode = _container.queryChild(position.offset, false).node; final targetNode = _container.queryChild(position.offset, false).node;
var targetChild = firstChild; var targetChild = firstChild;
while (targetChild != null) { while (targetChild != null) {
@ -1020,7 +1019,8 @@ class RenderEditableContainerBox extends RenderBox
} }
var child = firstChild; var child = firstChild;
double dx = -offset.dx, dy = _resolvedPadding!.top; final dx = -offset.dx;
var dy = _resolvedPadding!.top;
while (child != null) { while (child != null) {
if (child.size.contains(offset.translate(dx, -dy))) { if (child.size.contains(offset.translate(dx, -dy))) {
return child; return child;
@ -1047,16 +1047,15 @@ class RenderEditableContainerBox extends RenderBox
_resolvePadding(); _resolvePadding();
assert(_resolvedPadding != null); assert(_resolvedPadding != null);
double mainAxisExtent = _resolvedPadding!.top; var mainAxisExtent = _resolvedPadding!.top;
var child = firstChild; var child = firstChild;
BoxConstraints innerConstraints = final innerConstraints =
BoxConstraints.tightFor(width: constraints.maxWidth) BoxConstraints.tightFor(width: constraints.maxWidth)
.deflate(_resolvedPadding!); .deflate(_resolvedPadding!);
while (child != null) { while (child != null) {
child.layout(innerConstraints, parentUsesSize: true); child.layout(innerConstraints, parentUsesSize: true);
final EditableContainerParentData childParentData = final childParentData = (child.parentData as EditableContainerParentData)
child.parentData as EditableContainerParentData; ..offset = Offset(_resolvedPadding!.left, mainAxisExtent);
childParentData.offset = Offset(_resolvedPadding!.left, mainAxisExtent);
mainAxisExtent += child.size.height; mainAxisExtent += child.size.height;
assert(child.parentData == childParentData); assert(child.parentData == childParentData);
child = childParentData.nextSibling; child = childParentData.nextSibling;
@ -1068,24 +1067,22 @@ class RenderEditableContainerBox extends RenderBox
} }
double _getIntrinsicCrossAxis(double Function(RenderBox child) childSize) { double _getIntrinsicCrossAxis(double Function(RenderBox child) childSize) {
double extent = 0.0; var extent = 0.0;
var child = firstChild; var child = firstChild;
while (child != null) { while (child != null) {
extent = math.max(extent, childSize(child)); extent = math.max(extent, childSize(child));
EditableContainerParentData childParentData = final childParentData = child.parentData as EditableContainerParentData;
child.parentData as EditableContainerParentData;
child = childParentData.nextSibling; child = childParentData.nextSibling;
} }
return extent; return extent;
} }
double _getIntrinsicMainAxis(double Function(RenderBox child) childSize) { double _getIntrinsicMainAxis(double Function(RenderBox child) childSize) {
double extent = 0.0; var extent = 0.0;
var child = firstChild; var child = firstChild;
while (child != null) { while (child != null) {
extent += childSize(child); extent += childSize(child);
EditableContainerParentData childParentData = final childParentData = child.parentData as EditableContainerParentData;
child.parentData as EditableContainerParentData;
child = childParentData.nextSibling; child = childParentData.nextSibling;
} }
return extent; return extent;
@ -1094,9 +1091,9 @@ class RenderEditableContainerBox extends RenderBox
@override @override
double computeMinIntrinsicWidth(double height) { double computeMinIntrinsicWidth(double height) {
_resolvePadding(); _resolvePadding();
return _getIntrinsicCrossAxis((RenderBox child) { return _getIntrinsicCrossAxis((child) {
double childHeight = math.max( final childHeight = math.max<double>(
0.0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); 0, height - _resolvedPadding!.top + _resolvedPadding!.bottom);
return child.getMinIntrinsicWidth(childHeight) + return child.getMinIntrinsicWidth(childHeight) +
_resolvedPadding!.left + _resolvedPadding!.left +
_resolvedPadding!.right; _resolvedPadding!.right;
@ -1106,9 +1103,9 @@ class RenderEditableContainerBox extends RenderBox
@override @override
double computeMaxIntrinsicWidth(double height) { double computeMaxIntrinsicWidth(double height) {
_resolvePadding(); _resolvePadding();
return _getIntrinsicCrossAxis((RenderBox child) { return _getIntrinsicCrossAxis((child) {
double childHeight = math.max( final childHeight = math.max<double>(
0.0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); 0, height - _resolvedPadding!.top + _resolvedPadding!.bottom);
return child.getMaxIntrinsicWidth(childHeight) + return child.getMaxIntrinsicWidth(childHeight) +
_resolvedPadding!.left + _resolvedPadding!.left +
_resolvedPadding!.right; _resolvedPadding!.right;
@ -1118,9 +1115,9 @@ class RenderEditableContainerBox extends RenderBox
@override @override
double computeMinIntrinsicHeight(double width) { double computeMinIntrinsicHeight(double width) {
_resolvePadding(); _resolvePadding();
return _getIntrinsicMainAxis((RenderBox child) { return _getIntrinsicMainAxis((child) {
double childWidth = math.max( final childWidth = math.max<double>(
0.0, width - _resolvedPadding!.left + _resolvedPadding!.right); 0, width - _resolvedPadding!.left + _resolvedPadding!.right);
return child.getMinIntrinsicHeight(childWidth) + return child.getMinIntrinsicHeight(childWidth) +
_resolvedPadding!.top + _resolvedPadding!.top +
_resolvedPadding!.bottom; _resolvedPadding!.bottom;
@ -1130,9 +1127,9 @@ class RenderEditableContainerBox extends RenderBox
@override @override
double computeMaxIntrinsicHeight(double width) { double computeMaxIntrinsicHeight(double width) {
_resolvePadding(); _resolvePadding();
return _getIntrinsicMainAxis((RenderBox child) { return _getIntrinsicMainAxis((child) {
final childWidth = math.max( final childWidth = math.max<double>(
0.0, width - _resolvedPadding!.left + _resolvedPadding!.right); 0, width - _resolvedPadding!.left + _resolvedPadding!.right);
return child.getMaxIntrinsicHeight(childWidth) + return child.getMaxIntrinsicHeight(childWidth) +
_resolvedPadding!.top + _resolvedPadding!.top +
_resolvedPadding!.bottom; _resolvedPadding!.bottom;

@ -9,9 +9,12 @@ typedef InputShortcutCallback = void Function(InputShortcut? shortcut);
typedef OnDeleteCallback = void Function(bool forward); typedef OnDeleteCallback = void Function(bool forward);
class KeyboardListener { class KeyboardListener {
KeyboardListener(this.onCursorMove, this.onShortcut, this.onDelete);
final CursorMoveCallback onCursorMove; final CursorMoveCallback onCursorMove;
final InputShortcutCallback onShortcut; final InputShortcutCallback onShortcut;
final OnDeleteCallback onDelete; final OnDeleteCallback onDelete;
static final Set<LogicalKeyboardKey> _moveKeys = <LogicalKeyboardKey>{ static final Set<LogicalKeyboardKey> _moveKeys = <LogicalKeyboardKey>{
LogicalKeyboardKey.arrowRight, LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowLeft, LogicalKeyboardKey.arrowLeft,
@ -59,8 +62,6 @@ class KeyboardListener {
LogicalKeyboardKey.keyA: InputShortcut.SELECT_ALL, LogicalKeyboardKey.keyA: InputShortcut.SELECT_ALL,
}; };
KeyboardListener(this.onCursorMove, this.onShortcut, this.onDelete);
bool handleRawKeyEvent(RawKeyEvent event) { bool handleRawKeyEvent(RawKeyEvent event) {
if (kIsWeb) { if (kIsWeb) {
// On web platform, we should ignore the key because it's processed already. // On web platform, we should ignore the key because it's processed already.
@ -71,10 +72,10 @@ class KeyboardListener {
return false; return false;
} }
Set<LogicalKeyboardKey> keysPressed = final keysPressed =
LogicalKeyboardKey.collapseSynonyms(RawKeyboard.instance.keysPressed); LogicalKeyboardKey.collapseSynonyms(RawKeyboard.instance.keysPressed);
LogicalKeyboardKey key = event.logicalKey; final key = event.logicalKey;
bool isMacOS = event.data is RawKeyEventDataMacOs; final isMacOS = event.data is RawKeyEventDataMacOs;
if (!_nonModifierKeys.contains(key) || if (!_nonModifierKeys.contains(key) ||
keysPressed keysPressed
.difference(isMacOS ? _macOsModifierKeys : _modifierKeys) .difference(isMacOS ? _macOsModifierKeys : _modifierKeys)

@ -4,12 +4,12 @@ import 'package:flutter/widgets.dart';
import 'box.dart'; import 'box.dart';
class BaselineProxy extends SingleChildRenderObjectWidget { class BaselineProxy extends SingleChildRenderObjectWidget {
final TextStyle? textStyle;
final EdgeInsets? padding;
const BaselineProxy({Key? key, Widget? child, this.textStyle, this.padding}) const BaselineProxy({Key? key, Widget? child, this.textStyle, this.padding})
: super(key: key, child: child); : super(key: key, child: child);
final TextStyle? textStyle;
final EdgeInsets? padding;
@override @override
RenderBaselineProxy createRenderObject(BuildContext context) { RenderBaselineProxy createRenderObject(BuildContext context) {
return RenderBaselineProxy( return RenderBaselineProxy(
@ -87,14 +87,14 @@ class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox {
List<TextBox> getBoxesForSelection(TextSelection selection) { List<TextBox> getBoxesForSelection(TextSelection selection) {
if (!selection.isCollapsed) { if (!selection.isCollapsed) {
return <TextBox>[ return <TextBox>[
TextBox.fromLTRBD(0.0, 0.0, size.width, size.height, TextDirection.ltr) TextBox.fromLTRBD(0, 0, size.width, size.height, TextDirection.ltr)
]; ];
} }
double left = selection.extentOffset == 0 ? 0.0 : size.width; final left = selection.extentOffset == 0 ? 0.0 : size.width;
double right = selection.extentOffset == 0 ? 0.0 : size.width; final right = selection.extentOffset == 0 ? 0.0 : size.width;
return <TextBox>[ return <TextBox>[
TextBox.fromLTRBD(left, 0.0, right, size.height, TextDirection.ltr) TextBox.fromLTRBD(left, 0, right, size.height, TextDirection.ltr)
]; ];
} }
@ -104,7 +104,7 @@ class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox {
@override @override
Offset getOffsetForCaret(TextPosition position, Rect? caretPrototype) { Offset getOffsetForCaret(TextPosition position, Rect? caretPrototype) {
assert(position.offset <= 1 && position.offset >= 0); assert(position.offset <= 1 && position.offset >= 0);
return position.offset == 0 ? Offset.zero : Offset(size.width, 0.0); return position.offset == 0 ? Offset.zero : Offset(size.width, 0);
} }
@override @override
@ -122,6 +122,18 @@ class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox {
} }
class RichTextProxy extends SingleChildRenderObjectWidget { class RichTextProxy extends SingleChildRenderObjectWidget {
const RichTextProxy(
RichText child,
this.textStyle,
this.textAlign,
this.textDirection,
this.textScaleFactor,
this.locale,
this.strutStyle,
this.textWidthBasis,
this.textHeightBehavior,
) : super(child: child);
final TextStyle textStyle; final TextStyle textStyle;
final TextAlign textAlign; final TextAlign textAlign;
final TextDirection textDirection; final TextDirection textDirection;
@ -145,29 +157,18 @@ class RichTextProxy extends SingleChildRenderObjectWidget {
textHeightBehavior); textHeightBehavior);
} }
const RichTextProxy(
RichText child,
this.textStyle,
this.textAlign,
this.textDirection,
this.textScaleFactor,
this.locale,
this.strutStyle,
this.textWidthBasis,
this.textHeightBehavior)
: super(child: child);
@override @override
void updateRenderObject( void updateRenderObject(
BuildContext context, covariant RenderParagraphProxy renderObject) { BuildContext context, covariant RenderParagraphProxy renderObject) {
renderObject.textStyle = textStyle; renderObject
renderObject.textAlign = textAlign; ..textStyle = textStyle
renderObject.textDirection = textDirection; ..textAlign = textAlign
renderObject.textScaleFactor = textScaleFactor; ..textDirection = textDirection
renderObject.locale = locale; ..textScaleFactor = textScaleFactor
renderObject.strutStyle = strutStyle; ..locale = locale
renderObject.textWidthBasis = textWidthBasis; ..strutStyle = strutStyle
renderObject.textHeightBehavior = textHeightBehavior; ..textWidthBasis = textWidthBasis
..textHeightBehavior = textHeightBehavior;
} }
} }

@ -9,27 +9,58 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/documents/document.dart';
import 'package:flutter_quill/models/documents/nodes/block.dart';
import 'package:flutter_quill/models/documents/nodes/line.dart';
import 'package:flutter_quill/models/documents/nodes/node.dart';
import 'package:flutter_quill/utils/diff_delta.dart';
import 'package:flutter_quill/widgets/default_styles.dart';
import 'package:flutter_quill/widgets/proxy.dart';
import 'package:flutter_quill/widgets/text_block.dart';
import 'package:flutter_quill/widgets/text_line.dart';
import 'package:flutter_quill/widgets/text_selection.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import 'box.dart'; import '../models/documents/attribute.dart';
import '../models/documents/document.dart';
import '../models/documents/nodes/block.dart';
import '../models/documents/nodes/line.dart';
import '../utils/diff_delta.dart';
import 'controller.dart'; import 'controller.dart';
import 'cursor.dart'; import 'cursor.dart';
import 'default_styles.dart';
import 'delegate.dart'; import 'delegate.dart';
import 'editor.dart'; import 'editor.dart';
import 'keyboard_listener.dart'; import 'keyboard_listener.dart';
import 'proxy.dart';
import 'text_block.dart';
import 'text_line.dart';
import 'text_selection.dart';
class RawEditor extends StatefulWidget { class RawEditor extends StatefulWidget {
const RawEditor(
Key key,
this.controller,
this.focusNode,
this.scrollController,
this.scrollable,
this.padding,
this.readOnly,
this.placeholder,
this.onLaunchUrl,
this.toolbarOptions,
this.showSelectionHandles,
bool? showCursor,
this.cursorStyle,
this.textCapitalization,
this.maxHeight,
this.minHeight,
this.customStyles,
this.expands,
this.autoFocus,
this.selectionColor,
this.selectionCtrls,
this.keyboardAppearance,
this.enableInteractiveSelection,
this.scrollPhysics,
this.embedBuilder,
) : assert(maxHeight == null || maxHeight > 0, 'maxHeight cannot be null'),
assert(minHeight == null || minHeight >= 0, 'minHeight cannot be null'),
assert(maxHeight == null || minHeight == null || maxHeight >= minHeight,
'maxHeight cannot be null'),
showCursor = showCursor ?? true,
super(key: key);
final QuillController controller; final QuillController controller;
final FocusNode focusNode; final FocusNode focusNode;
final ScrollController scrollController; final ScrollController scrollController;
@ -55,39 +86,6 @@ class RawEditor extends StatefulWidget {
final ScrollPhysics? scrollPhysics; final ScrollPhysics? scrollPhysics;
final EmbedBuilder embedBuilder; final EmbedBuilder embedBuilder;
const RawEditor(
Key key,
this.controller,
this.focusNode,
this.scrollController,
this.scrollable,
this.padding,
this.readOnly,
this.placeholder,
this.onLaunchUrl,
this.toolbarOptions,
this.showSelectionHandles,
bool? showCursor,
this.cursorStyle,
this.textCapitalization,
this.maxHeight,
this.minHeight,
this.customStyles,
this.expands,
this.autoFocus,
this.selectionColor,
this.selectionCtrls,
this.keyboardAppearance,
this.enableInteractiveSelection,
this.scrollPhysics,
this.embedBuilder)
: assert(maxHeight == null || maxHeight > 0, 'maxHeight cannot be null'),
assert(minHeight == null || minHeight >= 0, 'minHeight cannot be null'),
assert(maxHeight == null || minHeight == null || maxHeight >= minHeight,
'maxHeight cannot be null'),
showCursor = showCursor ?? true,
super(key: key);
@override @override
State<StatefulWidget> createState() { State<StatefulWidget> createState() {
return RawEditorState(); return RawEditorState();
@ -140,7 +138,7 @@ class RawEditorState extends EditorState
bool get _hasFocus => widget.focusNode.hasFocus; bool get _hasFocus => widget.focusNode.hasFocus;
TextDirection get _textDirection { TextDirection get _textDirection {
TextDirection result = Directionality.of(context); final result = Directionality.of(context);
return result; return result;
} }
@ -153,13 +151,13 @@ class RawEditorState extends EditorState
if (wordModifier && lineModifier) { if (wordModifier && lineModifier) {
return; return;
} }
TextSelection selection = widget.controller.selection; final selection = widget.controller.selection;
TextSelection newSelection = widget.controller.selection; var newSelection = widget.controller.selection;
String plainText = textEditingValue.text; final plainText = textEditingValue.text;
bool rightKey = key == LogicalKeyboardKey.arrowRight, final rightKey = key == LogicalKeyboardKey.arrowRight,
leftKey = key == LogicalKeyboardKey.arrowLeft, leftKey = key == LogicalKeyboardKey.arrowLeft,
upKey = key == LogicalKeyboardKey.arrowUp, upKey = key == LogicalKeyboardKey.arrowUp,
downKey = key == LogicalKeyboardKey.arrowDown; downKey = key == LogicalKeyboardKey.arrowDown;
@ -184,7 +182,7 @@ class RawEditorState extends EditorState
TextSelection _placeCollapsedSelection(TextSelection selection, TextSelection _placeCollapsedSelection(TextSelection selection,
TextSelection newSelection, bool leftKey, bool rightKey) { TextSelection newSelection, bool leftKey, bool rightKey) {
int newOffset = newSelection.extentOffset; var newOffset = newSelection.extentOffset;
if (!selection.isCollapsed) { if (!selection.isCollapsed) {
if (leftKey) { if (leftKey) {
newOffset = newSelection.baseOffset < newSelection.extentOffset newOffset = newSelection.baseOffset < newSelection.extentOffset
@ -206,41 +204,38 @@ class RawEditorState extends EditorState
TextSelection selection, TextSelection selection,
TextSelection newSelection, TextSelection newSelection,
String plainText) { String plainText) {
TextPosition originPosition = TextPosition( final originPosition = TextPosition(
offset: upKey ? selection.baseOffset : selection.extentOffset); offset: upKey ? selection.baseOffset : selection.extentOffset);
RenderEditableBox child = final child = getRenderEditor()!.childAtPosition(originPosition);
getRenderEditor()!.childAtPosition(originPosition); final localPosition = TextPosition(
TextPosition localPosition = TextPosition( offset: originPosition.offset - child.getContainer().documentOffset);
offset:
originPosition.offset - child.getContainer().getDocumentOffset());
TextPosition? position = upKey var position = upKey
? child.getPositionAbove(localPosition) ? child.getPositionAbove(localPosition)
: child.getPositionBelow(localPosition); : child.getPositionBelow(localPosition);
if (position == null) { if (position == null) {
var sibling = upKey final sibling = upKey
? getRenderEditor()!.childBefore(child) ? getRenderEditor()!.childBefore(child)
: getRenderEditor()!.childAfter(child); : getRenderEditor()!.childAfter(child);
if (sibling == null) { if (sibling == null) {
position = TextPosition(offset: upKey ? 0 : plainText.length - 1); position = TextPosition(offset: upKey ? 0 : plainText.length - 1);
} else { } else {
Offset finalOffset = Offset( final finalOffset = Offset(
child.getOffsetForCaret(localPosition).dx, child.getOffsetForCaret(localPosition).dx,
sibling sibling
.getOffsetForCaret(TextPosition( .getOffsetForCaret(TextPosition(
offset: upKey ? sibling.getContainer().length - 1 : 0)) offset: upKey ? sibling.getContainer().length - 1 : 0))
.dy); .dy);
TextPosition siblingPosition = final siblingPosition = sibling.getPositionForOffset(finalOffset);
sibling.getPositionForOffset(finalOffset);
position = TextPosition( position = TextPosition(
offset: sibling.getContainer().getDocumentOffset() + offset:
siblingPosition.offset); sibling.getContainer().documentOffset + siblingPosition.offset);
} }
} else { } else {
position = TextPosition( position = TextPosition(
offset: child.getContainer().getDocumentOffset() + position.offset); offset: child.getContainer().documentOffset + position.offset);
} }
if (position.offset == newSelection.extentOffset) { if (position.offset == newSelection.extentOffset) {
@ -273,28 +268,28 @@ class RawEditorState extends EditorState
bool shift) { bool shift) {
if (wordModifier) { if (wordModifier) {
if (leftKey) { if (leftKey) {
TextSelection textSelection = getRenderEditor()!.selectWordAtPosition( final textSelection = getRenderEditor()!.selectWordAtPosition(
TextPosition( TextPosition(
offset: _previousCharacter( offset: _previousCharacter(
newSelection.extentOffset, plainText, false))); newSelection.extentOffset, plainText, false)));
return newSelection.copyWith(extentOffset: textSelection.baseOffset); return newSelection.copyWith(extentOffset: textSelection.baseOffset);
} }
TextSelection textSelection = getRenderEditor()!.selectWordAtPosition( final textSelection = getRenderEditor()!.selectWordAtPosition(
TextPosition( TextPosition(
offset: offset:
_nextCharacter(newSelection.extentOffset, plainText, false))); _nextCharacter(newSelection.extentOffset, plainText, false)));
return newSelection.copyWith(extentOffset: textSelection.extentOffset); return newSelection.copyWith(extentOffset: textSelection.extentOffset);
} else if (lineModifier) { } else if (lineModifier) {
if (leftKey) { if (leftKey) {
TextSelection textSelection = getRenderEditor()!.selectLineAtPosition( final textSelection = getRenderEditor()!.selectLineAtPosition(
TextPosition( TextPosition(
offset: _previousCharacter( offset: _previousCharacter(
newSelection.extentOffset, plainText, false))); newSelection.extentOffset, plainText, false)));
return newSelection.copyWith(extentOffset: textSelection.baseOffset); return newSelection.copyWith(extentOffset: textSelection.baseOffset);
} }
int startPoint = newSelection.extentOffset; final startPoint = newSelection.extentOffset;
if (startPoint < plainText.length) { if (startPoint < plainText.length) {
TextSelection textSelection = getRenderEditor()! final textSelection = getRenderEditor()!
.selectLineAtPosition(TextPosition(offset: startPoint)); .selectLineAtPosition(TextPosition(offset: startPoint));
return newSelection.copyWith(extentOffset: textSelection.extentOffset); return newSelection.copyWith(extentOffset: textSelection.extentOffset);
} }
@ -302,9 +297,9 @@ class RawEditorState extends EditorState
} }
if (rightKey && newSelection.extentOffset < plainText.length) { if (rightKey && newSelection.extentOffset < plainText.length) {
int nextExtent = final nextExtent =
_nextCharacter(newSelection.extentOffset, plainText, true); _nextCharacter(newSelection.extentOffset, plainText, true);
int distance = nextExtent - newSelection.extentOffset; final distance = nextExtent - newSelection.extentOffset;
newSelection = newSelection.copyWith(extentOffset: nextExtent); newSelection = newSelection.copyWith(extentOffset: nextExtent);
if (shift) { if (shift) {
_cursorResetLocation += distance; _cursorResetLocation += distance;
@ -313,9 +308,9 @@ class RawEditorState extends EditorState
} }
if (leftKey && newSelection.extentOffset > 0) { if (leftKey && newSelection.extentOffset > 0) {
int previousExtent = final previousExtent =
_previousCharacter(newSelection.extentOffset, plainText, true); _previousCharacter(newSelection.extentOffset, plainText, true);
int distance = newSelection.extentOffset - previousExtent; final distance = newSelection.extentOffset - previousExtent;
newSelection = newSelection.copyWith(extentOffset: previousExtent); newSelection = newSelection.copyWith(extentOffset: previousExtent);
if (shift) { if (shift) {
_cursorResetLocation -= distance; _cursorResetLocation -= distance;
@ -331,8 +326,8 @@ class RawEditorState extends EditorState
return string.length; return string.length;
} }
int count = 0; var count = 0;
Characters remain = string.characters.skipWhile((String currentString) { final remain = string.characters.skipWhile((currentString) {
if (count <= index) { if (count <= index) {
count += currentString.length; count += currentString.length;
return true; return true;
@ -351,9 +346,9 @@ class RawEditorState extends EditorState
return 0; return 0;
} }
int count = 0; var count = 0;
int? lastNonWhitespace; int? lastNonWhitespace;
for (String currentString in string.characters) { for (final currentString in string.characters) {
if (!includeWhitespace && if (!includeWhitespace &&
!WHITE_SPACE.contains( !WHITE_SPACE.contains(
currentString.characters.first.toString().codeUnitAt(0))) { currentString.characters.first.toString().codeUnitAt(0))) {
@ -411,7 +406,7 @@ class RawEditorState extends EditorState
return; return;
} }
TextEditingValue actualValue = textEditingValue.copyWith( final actualValue = textEditingValue.copyWith(
composing: _lastKnownRemoteTextEditingValue!.composing, composing: _lastKnownRemoteTextEditingValue!.composing,
); );
@ -419,7 +414,7 @@ class RawEditorState extends EditorState
return; return;
} }
bool shouldRemember = final shouldRemember =
textEditingValue.text != _lastKnownRemoteTextEditingValue!.text; textEditingValue.text != _lastKnownRemoteTextEditingValue!.text;
_lastKnownRemoteTextEditingValue = actualValue; _lastKnownRemoteTextEditingValue = actualValue;
_textInputConnection!.setEditingState(actualValue); _textInputConnection!.setEditingState(actualValue);
@ -456,13 +451,12 @@ class RawEditorState extends EditorState
return; return;
} }
TextEditingValue effectiveLastKnownValue = final effectiveLastKnownValue = _lastKnownRemoteTextEditingValue!;
_lastKnownRemoteTextEditingValue!;
_lastKnownRemoteTextEditingValue = value; _lastKnownRemoteTextEditingValue = value;
String oldText = effectiveLastKnownValue.text; final oldText = effectiveLastKnownValue.text;
String text = value.text; final text = value.text;
int cursorPosition = value.selection.extentOffset; final cursorPosition = value.selection.extentOffset;
Diff diff = getDiff(oldText, text, cursorPosition); final diff = getDiff(oldText, text, cursorPosition);
widget.controller.replaceText( widget.controller.replaceText(
diff.start, diff.deleted.length, diff.inserted, value.selection); diff.start, diff.deleted.length, diff.inserted, value.selection);
} }
@ -513,7 +507,7 @@ class RawEditorState extends EditorState
_focusAttachment!.reparent(); _focusAttachment!.reparent();
super.build(context); super.build(context);
Document _doc = widget.controller.document; var _doc = widget.controller.document;
if (_doc.isEmpty() && if (_doc.isEmpty() &&
!widget.focusNode.hasFocus && !widget.focusNode.hasFocus &&
widget.placeholder != null) { widget.placeholder != null) {
@ -540,7 +534,7 @@ class RawEditorState extends EditorState
); );
if (widget.scrollable) { if (widget.scrollable) {
EdgeInsets baselinePadding = final baselinePadding =
EdgeInsets.only(top: _styles!.paragraph!.verticalSpacing.item1); EdgeInsets.only(top: _styles!.paragraph!.verticalSpacing.item1);
child = BaselineProxy( child = BaselineProxy(
textStyle: _styles!.paragraph!.style, textStyle: _styles!.paragraph!.style,
@ -553,7 +547,7 @@ class RawEditorState extends EditorState
); );
} }
BoxConstraints constraints = widget.expands final constraints = widget.expands
? const BoxConstraints.expand() ? const BoxConstraints.expand()
: BoxConstraints( : BoxConstraints(
minHeight: widget.minHeight ?? 0.0, minHeight: widget.minHeight ?? 0.0,
@ -584,15 +578,14 @@ class RawEditorState extends EditorState
List<Widget> _buildChildren(Document doc, BuildContext context) { List<Widget> _buildChildren(Document doc, BuildContext context) {
final result = <Widget>[]; final result = <Widget>[];
Map<int, int> indentLevelCounts = {}; final indentLevelCounts = <int, int>{};
for (Node node in doc.root.children) { for (final node in doc.root.children) {
if (node is Line) { if (node is Line) {
EditableTextLine editableTextLine = final editableTextLine = _getEditableTextLineFromNode(node, context);
_getEditableTextLineFromNode(node, context);
result.add(editableTextLine); result.add(editableTextLine);
} else if (node is Block) { } else if (node is Block) {
Map<String, Attribute> attrs = node.style.attributes; final attrs = node.style.attributes;
EditableTextBlock editableTextBlock = EditableTextBlock( final editableTextBlock = EditableTextBlock(
node, node,
_textDirection, _textDirection,
_getVerticalSpacingForBlock(node, _styles), _getVerticalSpacingForBlock(node, _styles),
@ -602,7 +595,7 @@ class RawEditorState extends EditorState
widget.enableInteractiveSelection, widget.enableInteractiveSelection,
_hasFocus, _hasFocus,
attrs.containsKey(Attribute.codeBlock.key) attrs.containsKey(Attribute.codeBlock.key)
? const EdgeInsets.all(16.0) ? const EdgeInsets.all(16)
: null, : null,
widget.embedBuilder, widget.embedBuilder,
_cursorCont, _cursorCont,
@ -617,13 +610,13 @@ class RawEditorState extends EditorState
EditableTextLine _getEditableTextLineFromNode( EditableTextLine _getEditableTextLineFromNode(
Line node, BuildContext context) { Line node, BuildContext context) {
TextLine textLine = TextLine( final textLine = TextLine(
line: node, line: node,
textDirection: _textDirection, textDirection: _textDirection,
embedBuilder: widget.embedBuilder, embedBuilder: widget.embedBuilder,
styles: _styles!, styles: _styles!,
); );
EditableTextLine editableTextLine = EditableTextLine( final editableTextLine = EditableTextLine(
node, node,
null, null,
textLine, textLine,
@ -641,9 +634,9 @@ class RawEditorState extends EditorState
Tuple2<double, double> _getVerticalSpacingForLine( Tuple2<double, double> _getVerticalSpacingForLine(
Line line, DefaultStyles? defaultStyles) { Line line, DefaultStyles? defaultStyles) {
Map<String, Attribute> attrs = line.style.attributes; final attrs = line.style.attributes;
if (attrs.containsKey(Attribute.header.key)) { if (attrs.containsKey(Attribute.header.key)) {
int? level = attrs[Attribute.header.key]!.value; final int? level = attrs[Attribute.header.key]!.value;
switch (level) { switch (level) {
case 1: case 1:
return defaultStyles!.h1!.verticalSpacing; return defaultStyles!.h1!.verticalSpacing;
@ -661,7 +654,7 @@ class RawEditorState extends EditorState
Tuple2<double, double> _getVerticalSpacingForBlock( Tuple2<double, double> _getVerticalSpacingForBlock(
Block node, DefaultStyles? defaultStyles) { Block node, DefaultStyles? defaultStyles) {
Map<String, Attribute> attrs = node.style.attributes; final attrs = node.style.attributes;
if (attrs.containsKey(Attribute.blockQuote.key)) { if (attrs.containsKey(Attribute.blockQuote.key)) {
return defaultStyles!.quote!.verticalSpacing; return defaultStyles!.quote!.verticalSpacing;
} else if (attrs.containsKey(Attribute.codeBlock.key)) { } else if (attrs.containsKey(Attribute.codeBlock.key)) {
@ -704,7 +697,7 @@ class RawEditorState extends EditorState
_keyboardVisibilityController = KeyboardVisibilityController(); _keyboardVisibilityController = KeyboardVisibilityController();
_keyboardVisible = _keyboardVisibilityController!.isVisible; _keyboardVisible = _keyboardVisibilityController!.isVisible;
_keyboardVisibilitySubscription = _keyboardVisibilitySubscription =
_keyboardVisibilityController?.onChange.listen((bool visible) { _keyboardVisibilityController?.onChange.listen((visible) {
_keyboardVisible = visible; _keyboardVisible = visible;
if (visible) { if (visible) {
_onChangeTextEditingValue(); _onChangeTextEditingValue();
@ -720,8 +713,8 @@ class RawEditorState extends EditorState
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
DefaultStyles? parentStyles = QuillStyles.getStyles(context, true); final parentStyles = QuillStyles.getStyles(context, true);
DefaultStyles defaultStyles = DefaultStyles.getInstance(context); final defaultStyles = DefaultStyles.getInstance(context);
_styles = (parentStyles != null) _styles = (parentStyles != null)
? defaultStyles.merge(parentStyles) ? defaultStyles.merge(parentStyles)
: defaultStyles; : defaultStyles;
@ -784,27 +777,26 @@ class RawEditorState extends EditorState
} }
void handleDelete(bool forward) { void handleDelete(bool forward) {
TextSelection selection = widget.controller.selection; final selection = widget.controller.selection;
String plainText = textEditingValue.text; final plainText = textEditingValue.text;
int cursorPosition = selection.start; var cursorPosition = selection.start;
String textBefore = selection.textBefore(plainText); var textBefore = selection.textBefore(plainText);
String textAfter = selection.textAfter(plainText); var textAfter = selection.textAfter(plainText);
if (selection.isCollapsed) { if (selection.isCollapsed) {
if (!forward && textBefore.isNotEmpty) { if (!forward && textBefore.isNotEmpty) {
final int characterBoundary = final characterBoundary =
_previousCharacter(textBefore.length, textBefore, true); _previousCharacter(textBefore.length, textBefore, true);
textBefore = textBefore.substring(0, characterBoundary); textBefore = textBefore.substring(0, characterBoundary);
cursorPosition = characterBoundary; cursorPosition = characterBoundary;
} }
if (forward && textAfter.isNotEmpty && textAfter != '\n') { if (forward && textAfter.isNotEmpty && textAfter != '\n') {
final int deleteCount = _nextCharacter(0, textAfter, true); final deleteCount = _nextCharacter(0, textAfter, true);
textAfter = textAfter.substring(deleteCount); textAfter = textAfter.substring(deleteCount);
} }
} }
TextSelection newSelection = final newSelection = TextSelection.collapsed(offset: cursorPosition);
TextSelection.collapsed(offset: cursorPosition); final newText = textBefore + textAfter;
String newText = textBefore + textAfter; final size = plainText.length - newText.length;
int size = plainText.length - newText.length;
widget.controller.replaceText( widget.controller.replaceText(
cursorPosition, cursorPosition,
size, size,
@ -813,9 +805,9 @@ class RawEditorState extends EditorState
); );
} }
void handleShortcut(InputShortcut? shortcut) async { Future<void> handleShortcut(InputShortcut? shortcut) async {
TextSelection selection = widget.controller.selection; final selection = widget.controller.selection;
String plainText = textEditingValue.text; final plainText = textEditingValue.text;
if (shortcut == InputShortcut.COPY) { if (shortcut == InputShortcut.COPY) {
if (!selection.isCollapsed) { if (!selection.isCollapsed) {
await Clipboard.setData( await Clipboard.setData(
@ -844,7 +836,7 @@ class RawEditorState extends EditorState
return; return;
} }
if (shortcut == InputShortcut.PASTE && !widget.readOnly) { if (shortcut == InputShortcut.PASTE && !widget.readOnly) {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); final data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null) { if (data != null) {
widget.controller.replaceText( widget.controller.replaceText(
selection.start, selection.start,
@ -913,12 +905,13 @@ class RawEditorState extends EditorState
_cursorCont.startOrStopCursorTimerIfNeeded( _cursorCont.startOrStopCursorTimerIfNeeded(
_hasFocus, widget.controller.selection); _hasFocus, widget.controller.selection);
if (hasConnection) { if (hasConnection) {
_cursorCont.stopCursorTimer(resetCharTicks: false); _cursorCont
_cursorCont.startCursorTimer(); ..stopCursorTimer(resetCharTicks: false)
..startCursorTimer();
} }
SchedulerBinding.instance!.addPostFrameCallback( SchedulerBinding.instance!.addPostFrameCallback(
(Duration _) => _updateOrDisposeSelectionOverlayIfNeeded()); (_) => _updateOrDisposeSelectionOverlayIfNeeded());
if (mounted) { if (mounted) {
setState(() { setState(() {
// Use widget.controller.value in build() // Use widget.controller.value in build()
@ -989,12 +982,12 @@ class RawEditorState extends EditorState
} }
_showCaretOnScreenScheduled = true; _showCaretOnScreenScheduled = true;
SchedulerBinding.instance!.addPostFrameCallback((Duration _) { SchedulerBinding.instance!.addPostFrameCallback((_) {
_showCaretOnScreenScheduled = false; _showCaretOnScreenScheduled = false;
final viewport = RenderAbstractViewport.of(getRenderEditor())!; final viewport = RenderAbstractViewport.of(getRenderEditor())!;
final editorOffset = getRenderEditor()! final editorOffset = getRenderEditor()!
.localToGlobal(const Offset(0.0, 0.0), ancestor: viewport); .localToGlobal(const Offset(0, 0), ancestor: viewport);
final offsetInViewport = _scrollController!.offset + editorOffset.dy; final offsetInViewport = _scrollController!.offset + editorOffset.dy;
final offset = getRenderEditor()!.getOffsetToRevealCursor( final offset = getRenderEditor()!.getOffsetToRevealCursor(
@ -1065,7 +1058,7 @@ class RawEditorState extends EditorState
} }
} }
void __setEditingValue(TextEditingValue value) async { Future<void> __setEditingValue(TextEditingValue value) async {
if (await __isItCut(value)) { if (await __isItCut(value)) {
widget.controller.replaceText( widget.controller.replaceText(
textEditingValue.selection.start, textEditingValue.selection.start,
@ -1074,8 +1067,8 @@ class RawEditorState extends EditorState
value.selection, value.selection,
); );
} else { } else {
final TextEditingValue value = textEditingValue; final value = textEditingValue;
final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); final data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null) { if (data != null) {
final length = final length =
textEditingValue.selection.end - textEditingValue.selection.start; textEditingValue.selection.end - textEditingValue.selection.start;
@ -1095,7 +1088,7 @@ class RawEditorState extends EditorState
} }
Future<bool> __isItCut(TextEditingValue value) async { Future<bool> __isItCut(TextEditingValue value) async {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); final data = await Clipboard.getData(Clipboard.kTextPlain);
if (data == null) { if (data == null) {
return false; return false;
} }
@ -1175,14 +1168,15 @@ class _Editor extends MultiChildRenderObjectWidget {
@override @override
void updateRenderObject( void updateRenderObject(
BuildContext context, covariant RenderEditor renderObject) { BuildContext context, covariant RenderEditor renderObject) {
renderObject.document = document; renderObject
renderObject.setContainer(document.root); ..document = document
renderObject.textDirection = textDirection; ..setContainer(document.root)
renderObject.setHasFocus(hasFocus); ..textDirection = textDirection
renderObject.setSelection(selection); ..setHasFocus(hasFocus)
renderObject.setStartHandleLayerLink(startHandleLayerLink); ..setSelection(selection)
renderObject.setEndHandleLayerLink(endHandleLayerLink); ..setStartHandleLayerLink(startHandleLayerLink)
renderObject.onSelectionChanged = onSelectionChanged; ..setEndHandleLayerLink(endHandleLayerLink)
renderObject.setPadding(padding); ..onSelectionChanged = onSelectionChanged
..setPadding(padding);
} }
} }

@ -1,10 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ResponsiveWidget extends StatelessWidget { class ResponsiveWidget extends StatelessWidget {
final Widget largeScreen;
final Widget? mediumScreen;
final Widget? smallScreen;
const ResponsiveWidget({ const ResponsiveWidget({
required this.largeScreen, required this.largeScreen,
this.mediumScreen, this.mediumScreen,
@ -12,6 +8,10 @@ class ResponsiveWidget extends StatelessWidget {
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
final Widget largeScreen;
final Widget? mediumScreen;
final Widget? smallScreen;
static bool isSmallScreen(BuildContext context) { static bool isSmallScreen(BuildContext context) {
return MediaQuery.of(context).size.width < 800; return MediaQuery.of(context).size.width < 800;
} }

@ -1,19 +1,18 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/documents/nodes/block.dart';
import 'package:flutter_quill/models/documents/nodes/line.dart';
import 'package:flutter_quill/models/documents/nodes/node.dart';
import 'package:flutter_quill/widgets/cursor.dart';
import 'package:flutter_quill/widgets/default_styles.dart';
import 'package:flutter_quill/widgets/text_line.dart';
import 'package:flutter_quill/widgets/text_selection.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import '../models/documents/attribute.dart';
import '../models/documents/nodes/block.dart';
import '../models/documents/nodes/line.dart';
import 'box.dart'; import 'box.dart';
import 'cursor.dart';
import 'default_styles.dart';
import 'delegate.dart'; import 'delegate.dart';
import 'editor.dart'; import 'editor.dart';
import 'text_line.dart';
import 'text_selection.dart';
const List<int> arabianRomanNumbers = [ const List<int> arabianRomanNumbers = [
1000, 1000,
@ -48,6 +47,21 @@ const List<String> romanNumbers = [
]; ];
class EditableTextBlock extends StatelessWidget { class EditableTextBlock extends StatelessWidget {
const EditableTextBlock(
this.block,
this.textDirection,
this.verticalSpacing,
this.textSelection,
this.color,
this.styles,
this.enableInteractiveSelection,
this.hasFocus,
this.contentPadding,
this.embedBuilder,
this.cursorCont,
this.indentLevelCounts,
);
final Block block; final Block block;
final TextDirection textDirection; final TextDirection textDirection;
final Tuple2 verticalSpacing; final Tuple2 verticalSpacing;
@ -61,25 +75,11 @@ class EditableTextBlock extends StatelessWidget {
final CursorCont cursorCont; final CursorCont cursorCont;
final Map<int, int> indentLevelCounts; final Map<int, int> indentLevelCounts;
const EditableTextBlock(
this.block,
this.textDirection,
this.verticalSpacing,
this.textSelection,
this.color,
this.styles,
this.enableInteractiveSelection,
this.hasFocus,
this.contentPadding,
this.embedBuilder,
this.cursorCont,
this.indentLevelCounts);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasMediaQuery(context));
DefaultStyles? defaultStyles = QuillStyles.getStyles(context, false); final defaultStyles = QuillStyles.getStyles(context, false);
return _EditableBlock( return _EditableBlock(
block, block,
textDirection, textDirection,
@ -91,7 +91,7 @@ class EditableTextBlock extends StatelessWidget {
BoxDecoration? _getDecorationForBlock( BoxDecoration? _getDecorationForBlock(
Block node, DefaultStyles? defaultStyles) { Block node, DefaultStyles? defaultStyles) {
Map<String, Attribute> attrs = block.style.attributes; final attrs = block.style.attributes;
if (attrs.containsKey(Attribute.blockQuote.key)) { if (attrs.containsKey(Attribute.blockQuote.key)) {
return defaultStyles!.quote!.decoration; return defaultStyles!.quote!.decoration;
} }
@ -103,13 +103,13 @@ class EditableTextBlock extends StatelessWidget {
List<Widget> _buildChildren( List<Widget> _buildChildren(
BuildContext context, Map<int, int> indentLevelCounts) { BuildContext context, Map<int, int> indentLevelCounts) {
DefaultStyles? defaultStyles = QuillStyles.getStyles(context, false); final defaultStyles = QuillStyles.getStyles(context, false);
int count = block.children.length; final count = block.children.length;
var children = <Widget>[]; final children = <Widget>[];
int index = 0; var index = 0;
for (Line line in Iterable.castFrom<dynamic, Line>(block.children)) { for (final line in Iterable.castFrom<dynamic, Line>(block.children)) {
index++; index++;
EditableTextLine editableTextLine = EditableTextLine( final editableTextLine = EditableTextLine(
line, line,
_buildLeading(context, line, index, indentLevelCounts, count), _buildLeading(context, line, index, indentLevelCounts, count),
TextLine( TextLine(
@ -134,8 +134,8 @@ class EditableTextBlock extends StatelessWidget {
Widget? _buildLeading(BuildContext context, Line line, int index, Widget? _buildLeading(BuildContext context, Line line, int index,
Map<int, int> indentLevelCounts, int count) { Map<int, int> indentLevelCounts, int count) {
DefaultStyles? defaultStyles = QuillStyles.getStyles(context, false); final defaultStyles = QuillStyles.getStyles(context, false);
Map<String, Attribute> attrs = line.style.attributes; final attrs = line.style.attributes;
if (attrs[Attribute.list.key] == Attribute.ol) { if (attrs[Attribute.list.key] == Attribute.ol) {
return _NumberPoint( return _NumberPoint(
index: index, index: index,
@ -143,8 +143,8 @@ class EditableTextBlock extends StatelessWidget {
count: count, count: count,
style: defaultStyles!.leading!.style, style: defaultStyles!.leading!.style,
attrs: attrs, attrs: attrs,
width: 32.0, width: 32,
padding: 8.0, padding: 8,
); );
} }
@ -173,9 +173,9 @@ class EditableTextBlock extends StatelessWidget {
count: count, count: count,
style: defaultStyles!.code!.style style: defaultStyles!.code!.style
.copyWith(color: defaultStyles.code!.style.color!.withOpacity(0.4)), .copyWith(color: defaultStyles.code!.style.color!.withOpacity(0.4)),
width: 32.0, width: 32,
attrs: attrs, attrs: attrs,
padding: 16.0, padding: 16,
withDot: false, withDot: false,
); );
} }
@ -183,10 +183,10 @@ class EditableTextBlock extends StatelessWidget {
} }
double _getIndentWidth() { double _getIndentWidth() {
Map<String, Attribute> attrs = block.style.attributes; final attrs = block.style.attributes;
Attribute? indent = attrs[Attribute.indent.key]; final indent = attrs[Attribute.indent.key];
double extraIndent = 0.0; var extraIndent = 0.0;
if (indent != null && indent.value != null) { if (indent != null && indent.value != null) {
extraIndent = 16.0 * indent.value; extraIndent = 16.0 * indent.value;
} }
@ -200,11 +200,11 @@ class EditableTextBlock extends StatelessWidget {
Tuple2 _getSpacingForLine( Tuple2 _getSpacingForLine(
Line node, int index, int count, DefaultStyles? defaultStyles) { Line node, int index, int count, DefaultStyles? defaultStyles) {
double top = 0.0, bottom = 0.0; var top = 0.0, bottom = 0.0;
Map<String, Attribute> attrs = block.style.attributes; final attrs = block.style.attributes;
if (attrs.containsKey(Attribute.header.key)) { if (attrs.containsKey(Attribute.header.key)) {
int? level = attrs[Attribute.header.key]!.value; final level = attrs[Attribute.header.key]!.value;
switch (level) { switch (level) {
case 1: case 1:
top = defaultStyles!.h1!.verticalSpacing.item1; top = defaultStyles!.h1!.verticalSpacing.item1;
@ -310,22 +310,22 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
@override @override
TextRange getLineBoundary(TextPosition position) { TextRange getLineBoundary(TextPosition position) {
RenderEditableBox child = childAtPosition(position); final child = childAtPosition(position);
TextRange rangeInChild = child.getLineBoundary(TextPosition( final rangeInChild = child.getLineBoundary(TextPosition(
offset: position.offset - child.getContainer().getOffset(), offset: position.offset - child.getContainer().offset,
affinity: position.affinity, affinity: position.affinity,
)); ));
return TextRange( return TextRange(
start: rangeInChild.start + child.getContainer().getOffset(), start: rangeInChild.start + child.getContainer().offset,
end: rangeInChild.end + child.getContainer().getOffset(), end: rangeInChild.end + child.getContainer().offset,
); );
} }
@override @override
Offset getOffsetForCaret(TextPosition position) { Offset getOffsetForCaret(TextPosition position) {
RenderEditableBox child = childAtPosition(position); final child = childAtPosition(position);
return child.getOffsetForCaret(TextPosition( return child.getOffsetForCaret(TextPosition(
offset: position.offset - child.getContainer().getOffset(), offset: position.offset - child.getContainer().offset,
affinity: position.affinity, affinity: position.affinity,
)) + )) +
(child.parentData as BoxParentData).offset; (child.parentData as BoxParentData).offset;
@ -333,21 +333,21 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
@override @override
TextPosition getPositionForOffset(Offset offset) { TextPosition getPositionForOffset(Offset offset) {
RenderEditableBox child = childAtOffset(offset)!; final child = childAtOffset(offset)!;
BoxParentData parentData = child.parentData as BoxParentData; final parentData = child.parentData as BoxParentData;
TextPosition localPosition = final localPosition =
child.getPositionForOffset(offset - parentData.offset); child.getPositionForOffset(offset - parentData.offset);
return TextPosition( return TextPosition(
offset: localPosition.offset + child.getContainer().getOffset(), offset: localPosition.offset + child.getContainer().offset,
affinity: localPosition.affinity, affinity: localPosition.affinity,
); );
} }
@override @override
TextRange getWordBoundary(TextPosition position) { TextRange getWordBoundary(TextPosition position) {
RenderEditableBox child = childAtPosition(position); final child = childAtPosition(position);
int nodeOffset = child.getContainer().getOffset(); final nodeOffset = child.getContainer().offset;
TextRange childWord = child final childWord = child
.getWordBoundary(TextPosition(offset: position.offset - nodeOffset)); .getWordBoundary(TextPosition(offset: position.offset - nodeOffset));
return TextRange( return TextRange(
start: childWord.start + nodeOffset, start: childWord.start + nodeOffset,
@ -359,27 +359,26 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
TextPosition? getPositionAbove(TextPosition position) { TextPosition? getPositionAbove(TextPosition position) {
assert(position.offset < getContainer().length); assert(position.offset < getContainer().length);
RenderEditableBox child = childAtPosition(position); final child = childAtPosition(position);
TextPosition childLocalPosition = TextPosition( final childLocalPosition =
offset: position.offset - child.getContainer().getOffset()); TextPosition(offset: position.offset - child.getContainer().offset);
TextPosition? result = child.getPositionAbove(childLocalPosition); final result = child.getPositionAbove(childLocalPosition);
if (result != null) { if (result != null) {
return TextPosition( return TextPosition(offset: result.offset + child.getContainer().offset);
offset: result.offset + child.getContainer().getOffset());
} }
RenderEditableBox? sibling = childBefore(child); final sibling = childBefore(child);
if (sibling == null) { if (sibling == null) {
return null; return null;
} }
Offset caretOffset = child.getOffsetForCaret(childLocalPosition); final caretOffset = child.getOffsetForCaret(childLocalPosition);
TextPosition testPosition = final testPosition =
TextPosition(offset: sibling.getContainer().length - 1); TextPosition(offset: sibling.getContainer().length - 1);
Offset testOffset = sibling.getOffsetForCaret(testPosition); final testOffset = sibling.getOffsetForCaret(testPosition);
Offset finalOffset = Offset(caretOffset.dx, testOffset.dy); final finalOffset = Offset(caretOffset.dx, testOffset.dy);
return TextPosition( return TextPosition(
offset: sibling.getContainer().getOffset() + offset: sibling.getContainer().offset +
sibling.getPositionForOffset(finalOffset).offset); sibling.getPositionForOffset(finalOffset).offset);
} }
@ -387,46 +386,44 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
TextPosition? getPositionBelow(TextPosition position) { TextPosition? getPositionBelow(TextPosition position) {
assert(position.offset < getContainer().length); assert(position.offset < getContainer().length);
RenderEditableBox child = childAtPosition(position); final child = childAtPosition(position);
TextPosition childLocalPosition = TextPosition( final childLocalPosition =
offset: position.offset - child.getContainer().getOffset()); TextPosition(offset: position.offset - child.getContainer().offset);
TextPosition? result = child.getPositionBelow(childLocalPosition); final result = child.getPositionBelow(childLocalPosition);
if (result != null) { if (result != null) {
return TextPosition( return TextPosition(offset: result.offset + child.getContainer().offset);
offset: result.offset + child.getContainer().getOffset());
} }
RenderEditableBox? sibling = childAfter(child); final sibling = childAfter(child);
if (sibling == null) { if (sibling == null) {
return null; return null;
} }
Offset caretOffset = child.getOffsetForCaret(childLocalPosition); final caretOffset = child.getOffsetForCaret(childLocalPosition);
Offset testOffset = final testOffset = sibling.getOffsetForCaret(const TextPosition(offset: 0));
sibling.getOffsetForCaret(const TextPosition(offset: 0)); final finalOffset = Offset(caretOffset.dx, testOffset.dy);
Offset finalOffset = Offset(caretOffset.dx, testOffset.dy);
return TextPosition( return TextPosition(
offset: sibling.getContainer().getOffset() + offset: sibling.getContainer().offset +
sibling.getPositionForOffset(finalOffset).offset); sibling.getPositionForOffset(finalOffset).offset);
} }
@override @override
double preferredLineHeight(TextPosition position) { double preferredLineHeight(TextPosition position) {
RenderEditableBox child = childAtPosition(position); final child = childAtPosition(position);
return child.preferredLineHeight(TextPosition( return child.preferredLineHeight(
offset: position.offset - child.getContainer().getOffset())); TextPosition(offset: position.offset - child.getContainer().offset));
} }
@override @override
TextSelectionPoint getBaseEndpointForSelection(TextSelection selection) { TextSelectionPoint getBaseEndpointForSelection(TextSelection selection) {
if (selection.isCollapsed) { if (selection.isCollapsed) {
return TextSelectionPoint( return TextSelectionPoint(
Offset(0.0, preferredLineHeight(selection.extent)) + Offset(0, preferredLineHeight(selection.extent)) +
getOffsetForCaret(selection.extent), getOffsetForCaret(selection.extent),
null); null);
} }
Node? baseNode = getContainer().queryChild(selection.start, false).node; final baseNode = getContainer().queryChild(selection.start, false).node;
var baseChild = firstChild; var baseChild = firstChild;
while (baseChild != null) { while (baseChild != null) {
if (baseChild.getContainer() == baseNode) { if (baseChild.getContainer() == baseNode) {
@ -436,7 +433,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
} }
assert(baseChild != null); assert(baseChild != null);
TextSelectionPoint basePoint = baseChild!.getBaseEndpointForSelection( final basePoint = baseChild!.getBaseEndpointForSelection(
localSelection(baseChild.getContainer(), selection, true)); localSelection(baseChild.getContainer(), selection, true));
return TextSelectionPoint( return TextSelectionPoint(
basePoint.point + (baseChild.parentData as BoxParentData).offset, basePoint.point + (baseChild.parentData as BoxParentData).offset,
@ -447,12 +444,12 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
TextSelectionPoint getExtentEndpointForSelection(TextSelection selection) { TextSelectionPoint getExtentEndpointForSelection(TextSelection selection) {
if (selection.isCollapsed) { if (selection.isCollapsed) {
return TextSelectionPoint( return TextSelectionPoint(
Offset(0.0, preferredLineHeight(selection.extent)) + Offset(0, preferredLineHeight(selection.extent)) +
getOffsetForCaret(selection.extent), getOffsetForCaret(selection.extent),
null); null);
} }
Node? extentNode = getContainer().queryChild(selection.end, false).node; final extentNode = getContainer().queryChild(selection.end, false).node;
var extentChild = firstChild; var extentChild = firstChild;
while (extentChild != null) { while (extentChild != null) {
@ -463,7 +460,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
} }
assert(extentChild != null); assert(extentChild != null);
TextSelectionPoint extentPoint = extentChild!.getExtentEndpointForSelection( final extentPoint = extentChild!.getExtentEndpointForSelection(
localSelection(extentChild.getContainer(), selection, true)); localSelection(extentChild.getContainer(), selection, true));
return TextSelectionPoint( return TextSelectionPoint(
extentPoint.point + (extentChild.parentData as BoxParentData).offset, extentPoint.point + (extentChild.parentData as BoxParentData).offset,
@ -487,11 +484,11 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
void _paintDecoration(PaintingContext context, Offset offset) { void _paintDecoration(PaintingContext context, Offset offset) {
_painter ??= _decoration.createBoxPainter(markNeedsPaint); _painter ??= _decoration.createBoxPainter(markNeedsPaint);
EdgeInsets decorationPadding = resolvedPadding! - _contentPadding; final decorationPadding = resolvedPadding! - _contentPadding;
ImageConfiguration filledConfiguration = final filledConfiguration =
configuration.copyWith(size: decorationPadding.deflateSize(size)); configuration.copyWith(size: decorationPadding.deflateSize(size));
int debugSaveCount = context.canvas.getSaveCount(); final debugSaveCount = context.canvas.getSaveCount();
final decorationOffset = final decorationOffset =
offset.translate(decorationPadding.left, decorationPadding.top); offset.translate(decorationPadding.left, decorationPadding.top);
@ -511,16 +508,21 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
} }
class _EditableBlock extends MultiChildRenderObjectWidget { class _EditableBlock extends MultiChildRenderObjectWidget {
_EditableBlock(
this.block,
this.textDirection,
this.padding,
this.decoration,
this.contentPadding,
List<Widget> children,
) : super(children: children);
final Block block; final Block block;
final TextDirection textDirection; final TextDirection textDirection;
final Tuple2<double, double> padding; final Tuple2<double, double> padding;
final Decoration decoration; final Decoration decoration;
final EdgeInsets? contentPadding; final EdgeInsets? contentPadding;
_EditableBlock(this.block, this.textDirection, this.padding, this.decoration,
this.contentPadding, List<Widget> children)
: super(children: children);
EdgeInsets get _padding => EdgeInsets get _padding =>
EdgeInsets.only(top: padding.item1, bottom: padding.item2); EdgeInsets.only(top: padding.item1, bottom: padding.item2);
@ -540,24 +542,16 @@ class _EditableBlock extends MultiChildRenderObjectWidget {
@override @override
void updateRenderObject( void updateRenderObject(
BuildContext context, covariant RenderEditableTextBlock renderObject) { BuildContext context, covariant RenderEditableTextBlock renderObject) {
renderObject.setContainer(block); renderObject
renderObject.textDirection = textDirection; ..setContainer(block)
renderObject.setPadding(_padding); ..textDirection = textDirection
renderObject.decoration = decoration; ..setPadding(_padding)
renderObject.contentPadding = _contentPadding; ..decoration = decoration
..contentPadding = _contentPadding;
} }
} }
class _NumberPoint extends StatelessWidget { class _NumberPoint extends StatelessWidget {
final int index;
final Map<int?, int> indentLevelCounts;
final int count;
final TextStyle style;
final double width;
final Map<String, Attribute> attrs;
final bool withDot;
final double padding;
const _NumberPoint({ const _NumberPoint({
required this.index, required this.index,
required this.indentLevelCounts, required this.indentLevelCounts,
@ -570,9 +564,18 @@ class _NumberPoint extends StatelessWidget {
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
final int index;
final Map<int?, int> indentLevelCounts;
final int count;
final TextStyle style;
final double width;
final Map<String, Attribute> attrs;
final bool withDot;
final double padding;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String s = index.toString(); var s = index.toString();
int? level = 0; int? level = 0;
if (!attrs.containsKey(Attribute.indent.key) && if (!attrs.containsKey(Attribute.indent.key) &&
!indentLevelCounts.containsKey(1)) { !indentLevelCounts.containsKey(1)) {
@ -581,7 +584,7 @@ class _NumberPoint extends StatelessWidget {
alignment: AlignmentDirectional.topEnd, alignment: AlignmentDirectional.topEnd,
width: width, width: width,
padding: EdgeInsetsDirectional.only(end: padding), padding: EdgeInsetsDirectional.only(end: padding),
child: Text(withDot ? '$s.' : '$s', style: style), child: Text(withDot ? '$s.' : s, style: style),
); );
} }
if (attrs.containsKey(Attribute.indent.key)) { if (attrs.containsKey(Attribute.indent.key)) {
@ -595,7 +598,7 @@ class _NumberPoint extends StatelessWidget {
// last visited level is done, going up // last visited level is done, going up
indentLevelCounts.remove(level + 1); indentLevelCounts.remove(level + 1);
} }
int count = (indentLevelCounts[level] ?? 0) + 1; final count = (indentLevelCounts[level] ?? 0) + 1;
indentLevelCounts[level] = count; indentLevelCounts[level] = count;
s = count.toString(); s = count.toString();
@ -612,7 +615,7 @@ class _NumberPoint extends StatelessWidget {
alignment: AlignmentDirectional.topEnd, alignment: AlignmentDirectional.topEnd,
width: width, width: width,
padding: EdgeInsetsDirectional.only(end: padding), padding: EdgeInsetsDirectional.only(end: padding),
child: Text(withDot ? '$s.' : '$s', style: style), child: Text(withDot ? '$s.' : s, style: style),
); );
} }
@ -653,33 +656,34 @@ class _NumberPoint extends StatelessWidget {
} }
class _BulletPoint extends StatelessWidget { class _BulletPoint extends StatelessWidget {
final TextStyle style;
final double width;
const _BulletPoint({ const _BulletPoint({
required this.style, required this.style,
required this.width, required this.width,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
final TextStyle style;
final double width;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
alignment: AlignmentDirectional.topEnd, alignment: AlignmentDirectional.topEnd,
width: width, width: width,
padding: const EdgeInsetsDirectional.only(end: 13.0), padding: const EdgeInsetsDirectional.only(end: 13),
child: Text('', style: style), child: Text('', style: style),
); );
} }
} }
class _Checkbox extends StatefulWidget { class _Checkbox extends StatefulWidget {
const _Checkbox({Key? key, this.style, this.width, this.isChecked})
: super(key: key);
final TextStyle? style; final TextStyle? style;
final double? width; final double? width;
final bool? isChecked; final bool? isChecked;
const _Checkbox({Key? key, this.style, this.width, this.isChecked})
: super(key: key);
@override @override
__CheckboxState createState() => __CheckboxState(); __CheckboxState createState() => __CheckboxState();
} }
@ -708,7 +712,7 @@ class __CheckboxState extends State<_Checkbox> {
return Container( return Container(
alignment: AlignmentDirectional.topEnd, alignment: AlignmentDirectional.topEnd,
width: widget.width, width: widget.width,
padding: const EdgeInsetsDirectional.only(end: 13.0), padding: const EdgeInsetsDirectional.only(end: 13),
child: Checkbox( child: Checkbox(
value: widget.isChecked, value: widget.isChecked,
onChanged: _onCheckboxClicked, onChanged: _onCheckboxClicked,

@ -3,30 +3,23 @@ import 'dart:math' as math;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/documents/nodes/container.dart'
as container;
import 'package:flutter_quill/models/documents/nodes/leaf.dart' as leaf;
import 'package:flutter_quill/models/documents/nodes/leaf.dart';
import 'package:flutter_quill/models/documents/nodes/line.dart';
import 'package:flutter_quill/models/documents/nodes/node.dart';
import 'package:flutter_quill/models/documents/style.dart';
import 'package:flutter_quill/utils/color.dart';
import 'package:flutter_quill/widgets/cursor.dart';
import 'package:flutter_quill/widgets/proxy.dart';
import 'package:flutter_quill/widgets/text_selection.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import '../models/documents/attribute.dart';
import '../models/documents/nodes/container.dart' as container;
import '../models/documents/nodes/leaf.dart' as leaf;
import '../models/documents/nodes/leaf.dart';
import '../models/documents/nodes/line.dart';
import '../models/documents/nodes/node.dart';
import '../utils/color.dart';
import 'box.dart'; import 'box.dart';
import 'cursor.dart';
import 'default_styles.dart'; import 'default_styles.dart';
import 'delegate.dart'; import 'delegate.dart';
import 'proxy.dart';
import 'text_selection.dart';
class TextLine extends StatelessWidget { class TextLine extends StatelessWidget {
final Line line;
final TextDirection? textDirection;
final EmbedBuilder embedBuilder;
final DefaultStyles styles;
const TextLine({ const TextLine({
required this.line, required this.line,
required this.embedBuilder, required this.embedBuilder,
@ -35,21 +28,25 @@ class TextLine extends StatelessWidget {
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
final Line line;
final TextDirection? textDirection;
final EmbedBuilder embedBuilder;
final DefaultStyles styles;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasMediaQuery(context));
if (line.hasEmbed) { if (line.hasEmbed) {
Embed embed = line.children.single as Embed; final embed = line.children.single as Embed;
return EmbedProxy(embedBuilder(context, embed)); return EmbedProxy(embedBuilder(context, embed));
} }
TextSpan textSpan = _buildTextSpan(context); final textSpan = _buildTextSpan(context);
StrutStyle strutStyle = final strutStyle = StrutStyle.fromTextStyle(textSpan.style!);
StrutStyle.fromTextStyle(textSpan.style!, forceStrutHeight: true);
final textAlign = _getTextAlign(); final textAlign = _getTextAlign();
RichText child = RichText( final child = RichText(
text: TextSpan(children: [textSpan]), text: textSpan,
textAlign: textAlign, textAlign: textAlign,
textDirection: textDirection, textDirection: textDirection,
strutStyle: strutStyle, strutStyle: strutStyle,
@ -60,7 +57,7 @@ class TextLine extends StatelessWidget {
textSpan.style!, textSpan.style!,
textAlign, textAlign,
textDirection!, textDirection!,
1.0, 1,
Localizations.localeOf(context), Localizations.localeOf(context),
strutStyle, strutStyle,
TextWidthBasis.parent, TextWidthBasis.parent,
@ -82,20 +79,20 @@ class TextLine extends StatelessWidget {
} }
TextSpan _buildTextSpan(BuildContext context) { TextSpan _buildTextSpan(BuildContext context) {
DefaultStyles defaultStyles = styles; final defaultStyles = styles;
List<TextSpan> children = line.children final children = line.children
.map((node) => _getTextSpanFromNode(defaultStyles, node)) .map((node) => _getTextSpanFromNode(defaultStyles, node))
.toList(growable: false); .toList(growable: false);
TextStyle textStyle = const TextStyle(); var textStyle = const TextStyle();
if (line.style.containsKey(Attribute.placeholder.key)) { if (line.style.containsKey(Attribute.placeholder.key)) {
textStyle = defaultStyles.placeHolder!.style; textStyle = defaultStyles.placeHolder!.style;
return TextSpan(children: children, style: textStyle); return TextSpan(children: children, style: textStyle);
} }
Attribute? header = line.style.attributes[Attribute.header.key]; final header = line.style.attributes[Attribute.header.key];
Map<Attribute, TextStyle> m = { final m = <Attribute, TextStyle>{
Attribute.h1: defaultStyles.h1!.style, Attribute.h1: defaultStyles.h1!.style,
Attribute.h2: defaultStyles.h2!.style, Attribute.h2: defaultStyles.h2!.style,
Attribute.h3: defaultStyles.h3!.style, Attribute.h3: defaultStyles.h3!.style,
@ -103,7 +100,7 @@ class TextLine extends StatelessWidget {
textStyle = textStyle.merge(m[header] ?? defaultStyles.paragraph!.style); textStyle = textStyle.merge(m[header] ?? defaultStyles.paragraph!.style);
Attribute? block = line.style.getBlockExceptHeader(); final block = line.style.getBlockExceptHeader();
TextStyle? toMerge; TextStyle? toMerge;
if (block == Attribute.blockQuote) { if (block == Attribute.blockQuote) {
toMerge = defaultStyles.quote!.style; toMerge = defaultStyles.quote!.style;
@ -119,29 +116,28 @@ class TextLine extends StatelessWidget {
} }
TextSpan _getTextSpanFromNode(DefaultStyles defaultStyles, Node node) { TextSpan _getTextSpanFromNode(DefaultStyles defaultStyles, Node node) {
leaf.Text textNode = node as leaf.Text; final textNode = node as leaf.Text;
Style style = textNode.style; final style = textNode.style;
TextStyle res = const TextStyle(); var res = const TextStyle();
Map<String, TextStyle?> m = { <String, TextStyle?>{
Attribute.bold.key: defaultStyles.bold, Attribute.bold.key: defaultStyles.bold,
Attribute.italic.key: defaultStyles.italic, Attribute.italic.key: defaultStyles.italic,
Attribute.link.key: defaultStyles.link, Attribute.link.key: defaultStyles.link,
Attribute.underline.key: defaultStyles.underline, Attribute.underline.key: defaultStyles.underline,
Attribute.strikeThrough.key: defaultStyles.strikeThrough, Attribute.strikeThrough.key: defaultStyles.strikeThrough,
}; }.forEach((k, s) {
m.forEach((k, s) {
if (style.values.any((v) => v.key == k)) { if (style.values.any((v) => v.key == k)) {
res = _merge(res, s!); res = _merge(res, s!);
} }
}); });
Attribute? font = textNode.style.attributes[Attribute.font.key]; final font = textNode.style.attributes[Attribute.font.key];
if (font != null && font.value != null) { if (font != null && font.value != null) {
res = res.merge(TextStyle(fontFamily: font.value)); res = res.merge(TextStyle(fontFamily: font.value));
} }
Attribute? size = textNode.style.attributes[Attribute.size.key]; final size = textNode.style.attributes[Attribute.size.key];
if (size != null && size.value != null) { if (size != null && size.value != null) {
switch (size.value) { switch (size.value) {
case 'small': case 'small':
@ -154,7 +150,7 @@ class TextLine extends StatelessWidget {
res = res.merge(defaultStyles.sizeHuge); res = res.merge(defaultStyles.sizeHuge);
break; break;
default: default:
double? fontSize = double.tryParse(size.value); final fontSize = double.tryParse(size.value);
if (fontSize != null) { if (fontSize != null) {
res = res.merge(TextStyle(fontSize: fontSize)); res = res.merge(TextStyle(fontSize: fontSize));
} else { } else {
@ -163,7 +159,7 @@ class TextLine extends StatelessWidget {
} }
} }
Attribute? color = textNode.style.attributes[Attribute.color.key]; final color = textNode.style.attributes[Attribute.color.key];
if (color != null && color.value != null) { if (color != null && color.value != null) {
var textColor = defaultStyles.color; var textColor = defaultStyles.color;
if (color.value is String) { if (color.value is String) {
@ -174,7 +170,7 @@ class TextLine extends StatelessWidget {
} }
} }
Attribute? background = textNode.style.attributes[Attribute.background.key]; final background = textNode.style.attributes[Attribute.background.key];
if (background != null && background.value != null) { if (background != null && background.value != null) {
final backgroundColor = stringToColor(background.value); final backgroundColor = stringToColor(background.value);
res = res.merge(TextStyle(backgroundColor: backgroundColor)); res = res.merge(TextStyle(backgroundColor: backgroundColor));
@ -198,6 +194,21 @@ class TextLine extends StatelessWidget {
} }
class EditableTextLine extends RenderObjectWidget { class EditableTextLine extends RenderObjectWidget {
const EditableTextLine(
this.line,
this.leading,
this.body,
this.indentWidth,
this.verticalSpacing,
this.textDirection,
this.textSelection,
this.color,
this.enableInteractiveSelection,
this.hasFocus,
this.devicePixelRatio,
this.cursorCont,
);
final Line line; final Line line;
final Widget? leading; final Widget? leading;
final Widget body; final Widget body;
@ -211,20 +222,6 @@ class EditableTextLine extends RenderObjectWidget {
final double devicePixelRatio; final double devicePixelRatio;
final CursorCont cursorCont; final CursorCont cursorCont;
const EditableTextLine(
this.line,
this.leading,
this.body,
this.indentWidth,
this.verticalSpacing,
this.textDirection,
this.textSelection,
this.color,
this.enableInteractiveSelection,
this.hasFocus,
this.devicePixelRatio,
this.cursorCont);
@override @override
RenderObjectElement createElement() { RenderObjectElement createElement() {
return _TextLineElement(this); return _TextLineElement(this);
@ -247,15 +244,16 @@ class EditableTextLine extends RenderObjectWidget {
@override @override
void updateRenderObject( void updateRenderObject(
BuildContext context, covariant RenderEditableTextLine renderObject) { BuildContext context, covariant RenderEditableTextLine renderObject) {
renderObject.setLine(line); renderObject
renderObject.setPadding(_getPadding()); ..setLine(line)
renderObject.setTextDirection(textDirection); ..setPadding(_getPadding())
renderObject.setTextSelection(textSelection); ..setTextDirection(textDirection)
renderObject.setColor(color); ..setTextSelection(textSelection)
renderObject.setEnableInteractiveSelection(enableInteractiveSelection); ..setColor(color)
renderObject.hasFocus = hasFocus; ..setEnableInteractiveSelection(enableInteractiveSelection)
renderObject.setDevicePixelRatio(devicePixelRatio); ..hasFocus = hasFocus
renderObject.setCursorCont(cursorCont); ..setDevicePixelRatio(devicePixelRatio)
..setCursorCont(cursorCont);
} }
EdgeInsetsGeometry _getPadding() { EdgeInsetsGeometry _getPadding() {
@ -269,6 +267,18 @@ class EditableTextLine extends RenderObjectWidget {
enum TextLineSlot { LEADING, BODY } enum TextLineSlot { LEADING, BODY }
class RenderEditableTextLine extends RenderEditableBox { class RenderEditableTextLine extends RenderEditableBox {
RenderEditableTextLine(
this.line,
this.textDirection,
this.textSelection,
this.enableInteractiveSelection,
this.hasFocus,
this.devicePixelRatio,
this.padding,
this.color,
this.cursorCont,
);
RenderBox? _leading; RenderBox? _leading;
RenderContentProxyBox? _body; RenderContentProxyBox? _body;
Line line; Line line;
@ -286,17 +296,6 @@ class RenderEditableTextLine extends RenderEditableBox {
Rect? _caretPrototype; Rect? _caretPrototype;
final Map<TextLineSlot, RenderBox> children = <TextLineSlot, RenderBox>{}; final Map<TextLineSlot, RenderBox> children = <TextLineSlot, RenderBox>{};
RenderEditableTextLine(
this.line,
this.textDirection,
this.textSelection,
this.enableInteractiveSelection,
this.hasFocus,
this.devicePixelRatio,
this.padding,
this.color,
this.cursorCont);
Iterable<RenderBox> get _children sync* { Iterable<RenderBox> get _children sync* {
if (_leading != null) { if (_leading != null) {
yield _leading!; yield _leading!;
@ -347,7 +346,7 @@ class RenderEditableTextLine extends RenderEditableBox {
return; return;
} }
bool containsSelection = containsTextSelection(); final containsSelection = containsTextSelection();
if (attached && containsCursor()) { if (attached && containsCursor()) {
cursorCont.removeListener(markNeedsLayout); cursorCont.removeListener(markNeedsLayout);
cursorCont.color.removeListener(markNeedsPaint); cursorCont.color.removeListener(markNeedsPaint);
@ -403,8 +402,8 @@ class RenderEditableTextLine extends RenderEditableBox {
} }
bool containsTextSelection() { bool containsTextSelection() {
return line.getDocumentOffset() <= textSelection.end && return line.documentOffset <= textSelection.end &&
textSelection.start <= line.getDocumentOffset() + line.length - 1; textSelection.start <= line.documentOffset + line.length - 1;
} }
bool containsCursor() { bool containsCursor() {
@ -426,7 +425,7 @@ class RenderEditableTextLine extends RenderEditableBox {
} }
List<TextBox> _getBoxes(TextSelection textSelection) { List<TextBox> _getBoxes(TextSelection textSelection) {
BoxParentData? parentData = _body!.parentData as BoxParentData?; final parentData = _body!.parentData as BoxParentData?;
return _body!.getBoxesForSelection(textSelection).map((box) { return _body!.getBoxesForSelection(textSelection).map((box) {
return TextBox.fromLTRBD( return TextBox.fromLTRBD(
box.left + parentData!.offset.dx, box.left + parentData!.offset.dx,
@ -461,13 +460,13 @@ class RenderEditableTextLine extends RenderEditableBox {
TextSelection textSelection, bool first) { TextSelection textSelection, bool first) {
if (textSelection.isCollapsed) { if (textSelection.isCollapsed) {
return TextSelectionPoint( return TextSelectionPoint(
Offset(0.0, preferredLineHeight(textSelection.extent)) + Offset(0, preferredLineHeight(textSelection.extent)) +
getOffsetForCaret(textSelection.extent), getOffsetForCaret(textSelection.extent),
null); null);
} }
List<TextBox> boxes = _getBoxes(textSelection); final boxes = _getBoxes(textSelection);
assert(boxes.isNotEmpty); assert(boxes.isNotEmpty);
TextBox targetBox = first ? boxes.first : boxes.last; final targetBox = first ? boxes.first : boxes.last;
return TextSelectionPoint( return TextSelectionPoint(
Offset(first ? targetBox.start : targetBox.end, targetBox.bottom), Offset(first ? targetBox.start : targetBox.end, targetBox.bottom),
targetBox.direction); targetBox.direction);
@ -475,10 +474,10 @@ class RenderEditableTextLine extends RenderEditableBox {
@override @override
TextRange getLineBoundary(TextPosition position) { TextRange getLineBoundary(TextPosition position) {
double lineDy = getOffsetForCaret(position) final lineDy = getOffsetForCaret(position)
.translate(0.0, 0.5 * preferredLineHeight(position)) .translate(0, 0.5 * preferredLineHeight(position))
.dy; .dy;
List<TextBox> lineBoxes = final lineBoxes =
_getBoxes(TextSelection(baseOffset: 0, extentOffset: line.length - 1)) _getBoxes(TextSelection(baseOffset: 0, extentOffset: line.length - 1))
.where((element) => element.top < lineDy && element.bottom > lineDy) .where((element) => element.top < lineDy && element.bottom > lineDy)
.toList(growable: false); .toList(growable: false);
@ -506,7 +505,7 @@ class RenderEditableTextLine extends RenderEditableBox {
TextPosition? _getPosition(TextPosition textPosition, double dyScale) { TextPosition? _getPosition(TextPosition textPosition, double dyScale) {
assert(textPosition.offset < line.length); assert(textPosition.offset < line.length);
Offset offset = getOffsetForCaret(textPosition) final offset = getOffsetForCaret(textPosition)
.translate(0, dyScale * preferredLineHeight(textPosition)); .translate(0, dyScale * preferredLineHeight(textPosition));
if (_body!.size if (_body!.size
.contains(offset - (_body!.parentData as BoxParentData).offset)) { .contains(offset - (_body!.parentData as BoxParentData).offset)) {
@ -546,15 +545,13 @@ class RenderEditableTextLine extends RenderEditableBox {
switch (defaultTargetPlatform) { switch (defaultTargetPlatform) {
case TargetPlatform.iOS: case TargetPlatform.iOS:
case TargetPlatform.macOS: case TargetPlatform.macOS:
_caretPrototype = _caretPrototype = Rect.fromLTWH(0, 0, cursorWidth, cursorHeight + 2);
Rect.fromLTWH(0.0, 0.0, cursorWidth, cursorHeight + 2);
break; break;
case TargetPlatform.android: case TargetPlatform.android:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
case TargetPlatform.linux: case TargetPlatform.linux:
case TargetPlatform.windows: case TargetPlatform.windows:
_caretPrototype = _caretPrototype = Rect.fromLTWH(0, 2, cursorWidth, cursorHeight - 4.0);
Rect.fromLTWH(0.0, 2.0, cursorWidth, cursorHeight - 4.0);
break; break;
default: default:
throw 'Invalid platform'; throw 'Invalid platform';
@ -576,7 +573,7 @@ class RenderEditableTextLine extends RenderEditableBox {
@override @override
void detach() { void detach() {
super.detach(); super.detach();
for (RenderBox child in _children) { for (final child in _children) {
child.detach(); child.detach();
} }
if (containsCursor()) { if (containsCursor()) {
@ -597,7 +594,7 @@ class RenderEditableTextLine extends RenderEditableBox {
@override @override
List<DiagnosticsNode> debugDescribeChildren() { List<DiagnosticsNode> debugDescribeChildren() {
var value = <DiagnosticsNode>[]; final value = <DiagnosticsNode>[];
void add(RenderBox? child, String name) { void add(RenderBox? child, String name) {
if (child != null) { if (child != null) {
value.add(child.toDiagnosticsNode(name: name)); value.add(child.toDiagnosticsNode(name: name));
@ -615,14 +612,14 @@ class RenderEditableTextLine extends RenderEditableBox {
@override @override
double computeMinIntrinsicWidth(double height) { double computeMinIntrinsicWidth(double height) {
_resolvePadding(); _resolvePadding();
double horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
double verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
int leadingWidth = _leading == null final leadingWidth = _leading == null
? 0 ? 0
: _leading!.getMinIntrinsicWidth(height - verticalPadding) as int; : _leading!.getMinIntrinsicWidth(height - verticalPadding) as int;
int bodyWidth = _body == null final bodyWidth = _body == null
? 0 ? 0
: _body!.getMinIntrinsicWidth(math.max(0.0, height - verticalPadding)) : _body!.getMinIntrinsicWidth(math.max(0, height - verticalPadding))
as int; as int;
return horizontalPadding + leadingWidth + bodyWidth; return horizontalPadding + leadingWidth + bodyWidth;
} }
@ -630,14 +627,14 @@ class RenderEditableTextLine extends RenderEditableBox {
@override @override
double computeMaxIntrinsicWidth(double height) { double computeMaxIntrinsicWidth(double height) {
_resolvePadding(); _resolvePadding();
double horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
double verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
int leadingWidth = _leading == null final leadingWidth = _leading == null
? 0 ? 0
: _leading!.getMaxIntrinsicWidth(height - verticalPadding) as int; : _leading!.getMaxIntrinsicWidth(height - verticalPadding) as int;
int bodyWidth = _body == null final bodyWidth = _body == null
? 0 ? 0
: _body!.getMaxIntrinsicWidth(math.max(0.0, height - verticalPadding)) : _body!.getMaxIntrinsicWidth(math.max(0, height - verticalPadding))
as int; as int;
return horizontalPadding + leadingWidth + bodyWidth; return horizontalPadding + leadingWidth + bodyWidth;
} }
@ -645,11 +642,11 @@ class RenderEditableTextLine extends RenderEditableBox {
@override @override
double computeMinIntrinsicHeight(double width) { double computeMinIntrinsicHeight(double width) {
_resolvePadding(); _resolvePadding();
double horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
double verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
if (_body != null) { if (_body != null) {
return _body! return _body!
.getMinIntrinsicHeight(math.max(0.0, width - horizontalPadding)) + .getMinIntrinsicHeight(math.max(0, width - horizontalPadding)) +
verticalPadding; verticalPadding;
} }
return verticalPadding; return verticalPadding;
@ -658,11 +655,11 @@ class RenderEditableTextLine extends RenderEditableBox {
@override @override
double computeMaxIntrinsicHeight(double width) { double computeMaxIntrinsicHeight(double width) {
_resolvePadding(); _resolvePadding();
double horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
double verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
if (_body != null) { if (_body != null) {
return _body! return _body!
.getMaxIntrinsicHeight(math.max(0.0, width - horizontalPadding)) + .getMaxIntrinsicHeight(math.max(0, width - horizontalPadding)) +
verticalPadding; verticalPadding;
} }
return verticalPadding; return verticalPadding;
@ -697,8 +694,7 @@ class RenderEditableTextLine extends RenderEditableBox {
: _resolvedPadding!.right; : _resolvedPadding!.right;
_body!.layout(innerConstraints, parentUsesSize: true); _body!.layout(innerConstraints, parentUsesSize: true);
final bodyParentData = _body!.parentData as BoxParentData; (_body!.parentData as BoxParentData).offset =
bodyParentData.offset =
Offset(_resolvedPadding!.left, _resolvedPadding!.top); Offset(_resolvedPadding!.left, _resolvedPadding!.top);
if (_leading != null) { if (_leading != null) {
@ -707,8 +703,8 @@ class RenderEditableTextLine extends RenderEditableBox {
maxWidth: indentWidth, maxWidth: indentWidth,
maxHeight: _body!.size.height); maxHeight: _body!.size.height);
_leading!.layout(leadingConstraints, parentUsesSize: true); _leading!.layout(leadingConstraints, parentUsesSize: true);
final parentData = _leading!.parentData as BoxParentData; (_leading!.parentData as BoxParentData).offset =
parentData.offset = Offset(0.0, _resolvedPadding!.top); Offset(0, _resolvedPadding!.top);
} }
size = constraints.constrain(Size( size = constraints.constrain(Size(
@ -739,8 +735,8 @@ class RenderEditableTextLine extends RenderEditableBox {
final parentData = _body!.parentData as BoxParentData; final parentData = _body!.parentData as BoxParentData;
final effectiveOffset = offset + parentData.offset; final effectiveOffset = offset + parentData.offset;
if (enableInteractiveSelection && if (enableInteractiveSelection &&
line.getDocumentOffset() <= textSelection.end && line.documentOffset <= textSelection.end &&
textSelection.start <= line.getDocumentOffset() + line.length - 1) { textSelection.start <= line.documentOffset + line.length - 1) {
final local = localSelection(line, textSelection, false); final local = localSelection(line, textSelection, false);
_selectedRects ??= _body!.getBoxesForSelection( _selectedRects ??= _body!.getBoxesForSelection(
local, local,
@ -776,7 +772,7 @@ class RenderEditableTextLine extends RenderEditableBox {
void _paintCursor(PaintingContext context, Offset effectiveOffset) { void _paintCursor(PaintingContext context, Offset effectiveOffset) {
final position = TextPosition( final position = TextPosition(
offset: textSelection.extentOffset - line.getDocumentOffset(), offset: textSelection.extentOffset - line.documentOffset,
affinity: textSelection.base.affinity, affinity: textSelection.base.affinity,
); );
_cursorPainter.paint(context.canvas, effectiveOffset, position); _cursorPainter.paint(context.canvas, effectiveOffset, position);
@ -851,8 +847,8 @@ class _TextLineElement extends RenderObjectElement {
} }
void _mountChild(Widget? widget, TextLineSlot slot) { void _mountChild(Widget? widget, TextLineSlot slot) {
Element? oldChild = _slotToChildren[slot]; final oldChild = _slotToChildren[slot];
Element? newChild = updateChild(oldChild, widget, slot); final newChild = updateChild(oldChild, widget, slot);
if (oldChild != null) { if (oldChild != null) {
_slotToChildren.remove(slot); _slotToChildren.remove(slot);
} }
@ -875,8 +871,8 @@ class _TextLineElement extends RenderObjectElement {
} }
void _updateChild(Widget? widget, TextLineSlot slot) { void _updateChild(Widget? widget, TextLineSlot slot) {
Element? oldChild = _slotToChildren[slot]; final oldChild = _slotToChildren[slot];
Element? newChild = updateChild(oldChild, widget, slot); final newChild = updateChild(oldChild, widget, slot);
if (oldChild != null) { if (oldChild != null) {
_slotToChildren.remove(slot); _slotToChildren.remove(slot);
} }

@ -7,15 +7,15 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter_quill/models/documents/nodes/node.dart';
import '../models/documents/nodes/node.dart';
import 'editor.dart'; import 'editor.dart';
TextSelection localSelection(Node node, TextSelection selection, fromParent) { TextSelection localSelection(Node node, TextSelection selection, fromParent) {
int base = fromParent ? node.getOffset() : node.getDocumentOffset(); final base = fromParent ? node.offset : node.documentOffset;
assert(base <= selection.end && selection.start <= base + node.length - 1); assert(base <= selection.end && selection.start <= base + node.length - 1);
int offset = fromParent ? node.getOffset() : node.getDocumentOffset(); final offset = fromParent ? node.offset : node.documentOffset;
return selection.copyWith( return selection.copyWith(
baseOffset: math.max(selection.start - offset, 0), baseOffset: math.max(selection.start - offset, 0),
extentOffset: math.min(selection.end - offset, node.length - 1)); extentOffset: math.min(selection.end - offset, node.length - 1));
@ -24,6 +24,27 @@ TextSelection localSelection(Node node, TextSelection selection, fromParent) {
enum _TextSelectionHandlePosition { START, END } enum _TextSelectionHandlePosition { START, END }
class EditorTextSelectionOverlay { class EditorTextSelectionOverlay {
EditorTextSelectionOverlay(
this.value,
this.handlesVisible,
this.context,
this.debugRequiredFor,
this.toolbarLayerLink,
this.startHandleLayerLink,
this.endHandleLayerLink,
this.renderObject,
this.selectionCtrls,
this.selectionDelegate,
this.dragStartBehavior,
this.onSelectionHandleTapped,
this.clipboardStatus,
) {
final overlay = Overlay.of(context, rootOverlay: true)!;
_toolbarController = AnimationController(
duration: const Duration(milliseconds: 150), vsync: overlay);
}
TextEditingValue value; TextEditingValue value;
bool handlesVisible = false; bool handlesVisible = false;
final BuildContext context; final BuildContext context;
@ -41,26 +62,6 @@ class EditorTextSelectionOverlay {
List<OverlayEntry>? _handles; List<OverlayEntry>? _handles;
OverlayEntry? toolbar; OverlayEntry? toolbar;
EditorTextSelectionOverlay(
this.value,
this.handlesVisible,
this.context,
this.debugRequiredFor,
this.toolbarLayerLink,
this.startHandleLayerLink,
this.endHandleLayerLink,
this.renderObject,
this.selectionCtrls,
this.selectionDelegate,
this.dragStartBehavior,
this.onSelectionHandleTapped,
this.clipboardStatus) {
OverlayState overlay = Overlay.of(context, rootOverlay: true)!;
_toolbarController = AnimationController(
duration: const Duration(milliseconds: 150), vsync: overlay);
}
TextSelection get _selection => value.selection; TextSelection get _selection => value.selection;
Animation<double> get _toolbarOpacity => _toolbarController.view; Animation<double> get _toolbarOpacity => _toolbarController.view;
@ -99,7 +100,7 @@ class EditorTextSelectionOverlay {
toolbar = OverlayEntry(builder: _buildToolbar); toolbar = OverlayEntry(builder: _buildToolbar);
Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)! Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)!
.insert(toolbar!); .insert(toolbar!);
_toolbarController.forward(from: 0.0); _toolbarController.forward(from: 0);
} }
Widget _buildHandle( Widget _buildHandle(
@ -111,7 +112,7 @@ class EditorTextSelectionOverlay {
return Visibility( return Visibility(
visible: handlesVisible, visible: handlesVisible,
child: _TextSelectionHandleOverlay( child: _TextSelectionHandleOverlay(
onSelectionHandleChanged: (TextSelection? newSelection) { onSelectionHandleChanged: (newSelection) {
_handleSelectionHandleChanged(newSelection, position); _handleSelectionHandleChanged(newSelection, position);
}, },
onSelectionHandleTapped: onSelectionHandleTapped, onSelectionHandleTapped: onSelectionHandleTapped,
@ -155,32 +156,32 @@ class EditorTextSelectionOverlay {
default: default:
throw 'Invalid position'; throw 'Invalid position';
} }
selectionDelegate.textEditingValue = selectionDelegate
value.copyWith(selection: newSelection, composing: TextRange.empty); ..textEditingValue =
selectionDelegate.bringIntoView(textPosition); value.copyWith(selection: newSelection, composing: TextRange.empty)
..bringIntoView(textPosition);
} }
Widget _buildToolbar(BuildContext context) { Widget _buildToolbar(BuildContext context) {
List<TextSelectionPoint> endpoints = final endpoints = renderObject!.getEndpointsForSelection(_selection);
renderObject!.getEndpointsForSelection(_selection);
Rect editingRegion = Rect.fromPoints( final editingRegion = Rect.fromPoints(
renderObject!.localToGlobal(Offset.zero), renderObject!.localToGlobal(Offset.zero),
renderObject!.localToGlobal(renderObject!.size.bottomRight(Offset.zero)), renderObject!.localToGlobal(renderObject!.size.bottomRight(Offset.zero)),
); );
double baseLineHeight = renderObject!.preferredLineHeight(_selection.base); final baseLineHeight = renderObject!.preferredLineHeight(_selection.base);
double extentLineHeight = final extentLineHeight =
renderObject!.preferredLineHeight(_selection.extent); renderObject!.preferredLineHeight(_selection.extent);
double smallestLineHeight = math.min(baseLineHeight, extentLineHeight); final smallestLineHeight = math.min(baseLineHeight, extentLineHeight);
bool isMultiline = endpoints.last.point.dy - endpoints.first.point.dy > final isMultiline = endpoints.last.point.dy - endpoints.first.point.dy >
smallestLineHeight / 2; smallestLineHeight / 2;
double midX = isMultiline final midX = isMultiline
? editingRegion.width / 2 ? editingRegion.width / 2
: (endpoints.first.point.dx + endpoints.last.point.dx) / 2; : (endpoints.first.point.dx + endpoints.last.point.dx) / 2;
Offset midpoint = Offset( final midpoint = Offset(
midX, midX,
endpoints[0].point.dy - baseLineHeight, endpoints[0].point.dy - baseLineHeight,
); );
@ -232,10 +233,10 @@ class EditorTextSelectionOverlay {
assert(_handles == null); assert(_handles == null);
_handles = <OverlayEntry>[ _handles = <OverlayEntry>[
OverlayEntry( OverlayEntry(
builder: (BuildContext context) => builder: (context) =>
_buildHandle(context, _TextSelectionHandlePosition.START)), _buildHandle(context, _TextSelectionHandlePosition.START)),
OverlayEntry( OverlayEntry(
builder: (BuildContext context) => builder: (context) =>
_buildHandle(context, _TextSelectionHandlePosition.END)), _buildHandle(context, _TextSelectionHandlePosition.END)),
]; ];
@ -326,14 +327,14 @@ class _TextSelectionHandleOverlayState
void _handleDragStart(DragStartDetails details) {} void _handleDragStart(DragStartDetails details) {}
void _handleDragUpdate(DragUpdateDetails details) { void _handleDragUpdate(DragUpdateDetails details) {
TextPosition position = final position =
widget.renderObject!.getPositionForOffset(details.globalPosition); widget.renderObject!.getPositionForOffset(details.globalPosition);
if (widget.selection.isCollapsed) { if (widget.selection.isCollapsed) {
widget.onSelectionHandleChanged(TextSelection.fromPosition(position)); widget.onSelectionHandleChanged(TextSelection.fromPosition(position));
return; return;
} }
bool isNormalized = final isNormalized =
widget.selection.extentOffset >= widget.selection.baseOffset; widget.selection.extentOffset >= widget.selection.baseOffset;
TextSelection? newSelection; TextSelection? newSelection;
switch (widget.position) { switch (widget.position) {
@ -389,27 +390,26 @@ class _TextSelectionHandleOverlayState
break; break;
} }
TextPosition textPosition = final textPosition = widget.position == _TextSelectionHandlePosition.START
widget.position == _TextSelectionHandlePosition.START ? widget.selection.base
? widget.selection.base : widget.selection.extent;
: widget.selection.extent; final lineHeight = widget.renderObject!.preferredLineHeight(textPosition);
double lineHeight = widget.renderObject!.preferredLineHeight(textPosition); final handleAnchor =
Offset handleAnchor =
widget.selectionControls.getHandleAnchor(type!, lineHeight); widget.selectionControls.getHandleAnchor(type!, lineHeight);
Size handleSize = widget.selectionControls.getHandleSize(lineHeight); final handleSize = widget.selectionControls.getHandleSize(lineHeight);
Rect handleRect = Rect.fromLTWH( final handleRect = Rect.fromLTWH(
-handleAnchor.dx, -handleAnchor.dx,
-handleAnchor.dy, -handleAnchor.dy,
handleSize.width, handleSize.width,
handleSize.height, handleSize.height,
); );
Rect interactiveRect = handleRect.expandToInclude( final interactiveRect = handleRect.expandToInclude(
Rect.fromCircle( Rect.fromCircle(
center: handleRect.center, radius: kMinInteractiveDimension / 2), center: handleRect.center, radius: kMinInteractiveDimension / 2),
); );
RelativeRect padding = RelativeRect.fromLTRB( final padding = RelativeRect.fromLTRB(
math.max((interactiveRect.width - handleRect.width) / 2, 0), math.max((interactiveRect.width - handleRect.width) / 2, 0),
math.max((interactiveRect.height - handleRect.height) / 2, 0), math.max((interactiveRect.height - handleRect.height) / 2, 0),
math.max((interactiveRect.width - handleRect.width) / 2, 0), math.max((interactiveRect.width - handleRect.width) / 2, 0),
@ -657,13 +657,12 @@ class _EditorTextSelectionGestureDetectorState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Map<Type, GestureRecognizerFactory> gestures = final gestures = <Type, GestureRecognizerFactory>{};
<Type, GestureRecognizerFactory>{};
gestures[TapGestureRecognizer] = gestures[TapGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>( GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(debugOwner: this), () => TapGestureRecognizer(debugOwner: this),
(TapGestureRecognizer instance) { (instance) {
instance instance
..onTapDown = _handleTapDown ..onTapDown = _handleTapDown
..onTapUp = _handleTapUp ..onTapUp = _handleTapUp
@ -678,7 +677,7 @@ class _EditorTextSelectionGestureDetectorState
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>( GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer( () => LongPressGestureRecognizer(
debugOwner: this, kind: PointerDeviceKind.touch), debugOwner: this, kind: PointerDeviceKind.touch),
(LongPressGestureRecognizer instance) { (instance) {
instance instance
..onLongPressStart = _handleLongPressStart ..onLongPressStart = _handleLongPressStart
..onLongPressMoveUpdate = _handleLongPressMoveUpdate ..onLongPressMoveUpdate = _handleLongPressMoveUpdate
@ -694,7 +693,7 @@ class _EditorTextSelectionGestureDetectorState
GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>( GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
() => HorizontalDragGestureRecognizer( () => HorizontalDragGestureRecognizer(
debugOwner: this, kind: PointerDeviceKind.mouse), debugOwner: this, kind: PointerDeviceKind.mouse),
(HorizontalDragGestureRecognizer instance) { (instance) {
instance instance
..dragStartBehavior = DragStartBehavior.down ..dragStartBehavior = DragStartBehavior.down
..onStart = _handleDragStart ..onStart = _handleDragStart
@ -708,7 +707,7 @@ class _EditorTextSelectionGestureDetectorState
gestures[ForcePressGestureRecognizer] = gestures[ForcePressGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<ForcePressGestureRecognizer>( GestureRecognizerFactoryWithHandlers<ForcePressGestureRecognizer>(
() => ForcePressGestureRecognizer(debugOwner: this), () => ForcePressGestureRecognizer(debugOwner: this),
(ForcePressGestureRecognizer instance) { (instance) {
instance instance
..onStart = ..onStart =
widget.onForcePressStart != null ? _forcePressStarted : null widget.onForcePressStart != null ? _forcePressStarted : null

@ -5,31 +5,31 @@ import 'package:filesystem_picker/filesystem_picker.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/documents/nodes/embed.dart';
import 'package:flutter_quill/models/documents/style.dart';
import 'package:flutter_quill/utils/color.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import '../models/documents/attribute.dart';
import '../models/documents/nodes/embed.dart';
import '../models/documents/style.dart';
import '../utils/color.dart';
import 'controller.dart'; import 'controller.dart';
double iconSize = 18.0; double iconSize = 18;
double kToolbarHeight = iconSize * 2; double kToolbarHeight = iconSize * 2;
typedef OnImagePickCallback = Future<String> Function(File file); typedef OnImagePickCallback = Future<String> Function(File file);
typedef ImagePickImpl = Future<String> Function(ImageSource source); typedef ImagePickImpl = Future<String> Function(ImageSource source);
class InsertEmbedButton extends StatelessWidget { class InsertEmbedButton extends StatelessWidget {
final QuillController controller;
final IconData icon;
const InsertEmbedButton({ const InsertEmbedButton({
required this.controller, required this.controller,
required this.icon, required this.icon,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
final QuillController controller;
final IconData icon;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuillIconButton( return QuillIconButton(
@ -52,15 +52,15 @@ class InsertEmbedButton extends StatelessWidget {
} }
class LinkStyleButton extends StatefulWidget { class LinkStyleButton extends StatefulWidget {
final QuillController controller;
final IconData? icon;
const LinkStyleButton({ const LinkStyleButton({
required this.controller, required this.controller,
this.icon, this.icon,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
final QuillController controller;
final IconData? icon;
@override @override
_LinkStyleButtonState createState() => _LinkStyleButtonState(); _LinkStyleButtonState createState() => _LinkStyleButtonState();
} }
@ -174,14 +174,6 @@ typedef ToggleStyleButtonBuilder = Widget Function(
); );
class ToggleStyleButton extends StatefulWidget { class ToggleStyleButton extends StatefulWidget {
final Attribute attribute;
final IconData icon;
final QuillController controller;
final ToggleStyleButtonBuilder childBuilder;
const ToggleStyleButton({ const ToggleStyleButton({
required this.attribute, required this.attribute,
required this.icon, required this.icon,
@ -190,6 +182,14 @@ class ToggleStyleButton extends StatefulWidget {
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
final Attribute attribute;
final IconData icon;
final QuillController controller;
final ToggleStyleButtonBuilder childBuilder;
@override @override
_ToggleStyleButtonState createState() => _ToggleStyleButtonState(); _ToggleStyleButtonState createState() => _ToggleStyleButtonState();
} }
@ -215,7 +215,7 @@ class _ToggleStyleButtonState extends State<ToggleStyleButton> {
bool _getIsToggled(Map<String, Attribute> attrs) { bool _getIsToggled(Map<String, Attribute> attrs) {
if (widget.attribute.key == Attribute.list.key) { if (widget.attribute.key == Attribute.list.key) {
Attribute? attribute = attrs[widget.attribute.key]; final attribute = attrs[widget.attribute.key];
if (attribute == null) { if (attribute == null) {
return false; return false;
} }
@ -258,14 +258,6 @@ class _ToggleStyleButtonState extends State<ToggleStyleButton> {
} }
class ToggleCheckListButton extends StatefulWidget { class ToggleCheckListButton extends StatefulWidget {
final IconData icon;
final QuillController controller;
final ToggleStyleButtonBuilder childBuilder;
final Attribute attribute;
const ToggleCheckListButton({ const ToggleCheckListButton({
required this.icon, required this.icon,
required this.controller, required this.controller,
@ -274,6 +266,14 @@ class ToggleCheckListButton extends StatefulWidget {
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
final IconData icon;
final QuillController controller;
final ToggleStyleButtonBuilder childBuilder;
final Attribute attribute;
@override @override
_ToggleCheckListButtonState createState() => _ToggleCheckListButtonState(); _ToggleCheckListButtonState createState() => _ToggleCheckListButtonState();
} }
@ -299,7 +299,7 @@ class _ToggleCheckListButtonState extends State<ToggleCheckListButton> {
bool _getIsToggled(Map<String, Attribute> attrs) { bool _getIsToggled(Map<String, Attribute> attrs) {
if (widget.attribute.key == Attribute.list.key) { if (widget.attribute.key == Attribute.list.key) {
Attribute? attribute = attrs[widget.attribute.key]; final attribute = attrs[widget.attribute.key];
if (attribute == null) { if (attribute == null) {
return false; return false;
} }
@ -369,11 +369,11 @@ Widget defaultToggleStyleButtonBuilder(
} }
class SelectHeaderStyleButton extends StatefulWidget { class SelectHeaderStyleButton extends StatefulWidget {
final QuillController controller;
const SelectHeaderStyleButton({required this.controller, Key? key}) const SelectHeaderStyleButton({required this.controller, Key? key})
: super(key: key); : super(key: key);
final QuillController controller;
@override @override
_SelectHeaderStyleButtonState createState() => _SelectHeaderStyleButtonState createState() =>
_SelectHeaderStyleButtonState(); _SelectHeaderStyleButtonState();
@ -430,20 +430,20 @@ class _SelectHeaderStyleButtonState extends State<SelectHeaderStyleButton> {
Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value, Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value,
ValueChanged<Attribute?> onSelected) { ValueChanged<Attribute?> onSelected) {
final Map<Attribute, String> _valueToText = { final _valueToText = <Attribute, String>{
Attribute.header: 'N', Attribute.header: 'N',
Attribute.h1: 'H1', Attribute.h1: 'H1',
Attribute.h2: 'H2', Attribute.h2: 'H2',
Attribute.h3: 'H3', Attribute.h3: 'H3',
}; };
List<Attribute> _valueAttribute = [ final _valueAttribute = <Attribute>[
Attribute.header, Attribute.header,
Attribute.h1, Attribute.h1,
Attribute.h2, Attribute.h2,
Attribute.h3 Attribute.h3
]; ];
List<String> _valueString = ['N', 'H1', 'H2', 'H3']; final _valueString = <String>['N', 'H1', 'H2', 'H3'];
final theme = Theme.of(context); final theme = Theme.of(context);
final style = TextStyle( final style = TextStyle(
@ -464,7 +464,7 @@ Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value,
child: RawMaterialButton( child: RawMaterialButton(
hoverElevation: 0, hoverElevation: 0,
highlightElevation: 0, highlightElevation: 0,
elevation: 0.0, elevation: 0,
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
shape: shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)),
@ -490,6 +490,15 @@ Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value,
} }
class ImageButton extends StatefulWidget { class ImageButton extends StatefulWidget {
const ImageButton({
required this.icon,
required this.controller,
required this.imageSource,
this.onImagePickCallback,
this.imagePickImpl,
Key? key,
}) : super(key: key);
final IconData icon; final IconData icon;
final QuillController controller; final QuillController controller;
@ -500,15 +509,6 @@ class ImageButton extends StatefulWidget {
final ImageSource imageSource; final ImageSource imageSource;
const ImageButton({
required this.icon,
required this.controller,
required this.imageSource,
this.onImagePickCallback,
this.imagePickImpl,
Key? key,
}) : super(key: key);
@override @override
_ImageButtonState createState() => _ImageButtonState(); _ImageButtonState createState() => _ImageButtonState();
} }
@ -520,10 +520,10 @@ class _ImageButtonState extends State<ImageButton> {
final FileType _pickingType = FileType.any; final FileType _pickingType = FileType.any;
Future<String?> _pickImage(ImageSource source) async { Future<String?> _pickImage(ImageSource source) async {
final PickedFile? pickedFile = await _picker.getImage(source: source); final pickedFile = await _picker.getImage(source: source);
if (pickedFile == null) return null; if (pickedFile == null) return null;
final File file = File(pickedFile.path); final file = File(pickedFile.path);
return widget.onImagePickCallback!(file); return widget.onImagePickCallback!(file);
} }
@ -536,11 +536,11 @@ class _ImageButtonState extends State<ImageButton> {
: null, : null,
)) ))
?.files; ?.files;
var _fileName = final _fileName =
_paths != null ? _paths!.map((e) => e.name).toString() : '...'; _paths != null ? _paths!.map((e) => e.name).toString() : '...';
if (_paths != null) { if (_paths != null) {
File file = File(_fileName); final file = File(_fileName);
// We simply return the absolute path to selected file. // We simply return the absolute path to selected file.
return widget.onImagePickCallback!(file); return widget.onImagePickCallback!(file);
} else { } else {
@ -550,7 +550,7 @@ class _ImageButtonState extends State<ImageButton> {
} }
Future<String> _pickImageDesktop() async { Future<String> _pickImageDesktop() async {
var filePath = await FilesystemPicker.open( final filePath = await FilesystemPicker.open(
context: context, context: context,
rootDirectory: await getApplicationDocumentsDirectory(), rootDirectory: await getApplicationDocumentsDirectory(),
fsType: FilesystemType.file, fsType: FilesystemType.file,
@ -558,7 +558,7 @@ class _ImageButtonState extends State<ImageButton> {
); );
if (filePath != null && filePath.isEmpty) return ''; if (filePath != null && filePath.isEmpty) return '';
final File file = File(filePath!); final file = File(filePath!);
return widget.onImagePickCallback!(file); return widget.onImagePickCallback!(file);
} }
@ -602,10 +602,6 @@ class _ImageButtonState extends State<ImageButton> {
/// When pressed, this button displays overlay toolbar with /// When pressed, this button displays overlay toolbar with
/// buttons for each color. /// buttons for each color.
class ColorButton extends StatefulWidget { class ColorButton extends StatefulWidget {
final IconData icon;
final bool background;
final QuillController controller;
const ColorButton({ const ColorButton({
required this.icon, required this.icon,
required this.controller, required this.controller,
@ -613,6 +609,10 @@ class ColorButton extends StatefulWidget {
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
final IconData icon;
final bool background;
final QuillController controller;
@override @override
_ColorButtonState createState() => _ColorButtonState(); _ColorButtonState createState() => _ColorButtonState();
} }
@ -683,19 +683,19 @@ class _ColorButtonState extends State<ColorButton> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
Color? iconColor = _isToggledColor && !widget.background && !_isWhite final iconColor = _isToggledColor && !widget.background && !_isWhite
? stringToColor(_selectionStyle.attributes['color']!.value) ? stringToColor(_selectionStyle.attributes['color']!.value)
: theme.iconTheme.color; : theme.iconTheme.color;
var iconColorBackground = final iconColorBackground =
_isToggledBackground && widget.background && !_isWhitebackground _isToggledBackground && widget.background && !_isWhitebackground
? stringToColor(_selectionStyle.attributes['background']!.value) ? stringToColor(_selectionStyle.attributes['background']!.value)
: theme.iconTheme.color; : theme.iconTheme.color;
Color fillColor = _isToggledColor && !widget.background && _isWhite final fillColor = _isToggledColor && !widget.background && _isWhite
? stringToColor('#ffffff') ? stringToColor('#ffffff')
: theme.canvasColor; : theme.canvasColor;
Color fillColorBackground = final fillColorBackground =
_isToggledBackground && widget.background && _isWhitebackground _isToggledBackground && widget.background && _isWhitebackground
? stringToColor('#ffffff') ? stringToColor('#ffffff')
: theme.canvasColor; : theme.canvasColor;
@ -713,7 +713,7 @@ class _ColorButtonState extends State<ColorButton> {
} }
void _changeColor(Color color) { void _changeColor(Color color) {
String hex = color.value.toRadixString(16); var hex = color.value.toRadixString(16);
if (hex.startsWith('ff')) { if (hex.startsWith('ff')) {
hex = hex.substring(2); hex = hex.substring(2);
} }
@ -740,10 +740,6 @@ class _ColorButtonState extends State<ColorButton> {
} }
class HistoryButton extends StatefulWidget { class HistoryButton extends StatefulWidget {
final IconData icon;
final bool undo;
final QuillController controller;
const HistoryButton({ const HistoryButton({
required this.icon, required this.icon,
required this.controller, required this.controller,
@ -751,6 +747,10 @@ class HistoryButton extends StatefulWidget {
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
final IconData icon;
final bool undo;
final QuillController controller;
@override @override
_HistoryButtonState createState() => _HistoryButtonState(); _HistoryButtonState createState() => _HistoryButtonState();
} }
@ -812,10 +812,6 @@ class _HistoryButtonState extends State<HistoryButton> {
} }
class IndentButton extends StatefulWidget { class IndentButton extends StatefulWidget {
final IconData icon;
final QuillController controller;
final bool isIncrease;
const IndentButton({ const IndentButton({
required this.icon, required this.icon,
required this.controller, required this.controller,
@ -823,6 +819,10 @@ class IndentButton extends StatefulWidget {
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
final IconData icon;
final QuillController controller;
final bool isIncrease;
@override @override
_IndentButtonState createState() => _IndentButtonState(); _IndentButtonState createState() => _IndentButtonState();
} }
@ -867,16 +867,16 @@ class _IndentButtonState extends State<IndentButton> {
} }
class ClearFormatButton extends StatefulWidget { class ClearFormatButton extends StatefulWidget {
final IconData icon;
final QuillController controller;
const ClearFormatButton({ const ClearFormatButton({
required this.icon, required this.icon,
required this.controller, required this.controller,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
final IconData icon;
final QuillController controller;
@override @override
_ClearFormatButtonState createState() => _ClearFormatButtonState(); _ClearFormatButtonState createState() => _ClearFormatButtonState();
} }
@ -894,7 +894,7 @@ class _ClearFormatButtonState extends State<ClearFormatButton> {
icon: Icon(widget.icon, size: iconSize, color: iconColor), icon: Icon(widget.icon, size: iconSize, color: iconColor),
fillColor: fillColor, fillColor: fillColor,
onPressed: () { onPressed: () {
for (Attribute k for (final k
in widget.controller.getSelectionStyle().attributes.values) { in widget.controller.getSelectionStyle().attributes.values) {
widget.controller.formatSelection(Attribute.clone(k, null)); widget.controller.formatSelection(Attribute.clone(k, null));
} }
@ -903,8 +903,6 @@ class _ClearFormatButtonState extends State<ClearFormatButton> {
} }
class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
final List<Widget> children;
const QuillToolbar({required this.children, Key? key}) : super(key: key); const QuillToolbar({required this.children, Key? key}) : super(key: key);
factory QuillToolbar.basic({ factory QuillToolbar.basic({
@ -1117,6 +1115,8 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
]); ]);
} }
final List<Widget> children;
@override @override
_QuillToolbarState createState() => _QuillToolbarState(); _QuillToolbarState createState() => _QuillToolbarState();
@ -1148,13 +1148,6 @@ class _QuillToolbarState extends State<QuillToolbar> {
} }
class QuillIconButton extends StatelessWidget { class QuillIconButton extends StatelessWidget {
final VoidCallback? onPressed;
final Widget? icon;
final double size;
final Color? fillColor;
final double hoverElevation;
final double highlightElevation;
const QuillIconButton({ const QuillIconButton({
required this.onPressed, required this.onPressed,
this.icon, this.icon,
@ -1165,6 +1158,13 @@ class QuillIconButton extends StatelessWidget {
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
final VoidCallback? onPressed;
final Widget? icon;
final double size;
final Color? fillColor;
final double hoverElevation;
final double highlightElevation;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ConstrainedBox( return ConstrainedBox(
@ -1184,15 +1184,6 @@ class QuillIconButton extends StatelessWidget {
} }
class QuillDropdownButton<T> extends StatefulWidget { class QuillDropdownButton<T> extends StatefulWidget {
final double height;
final Color? fillColor;
final double hoverElevation;
final double highlightElevation;
final Widget child;
final T initialValue;
final List<PopupMenuEntry<T>> items;
final ValueChanged<T> onSelected;
const QuillDropdownButton({ const QuillDropdownButton({
required this.child, required this.child,
required this.initialValue, required this.initialValue,
@ -1205,6 +1196,15 @@ class QuillDropdownButton<T> extends StatefulWidget {
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
final double height;
final Color? fillColor;
final double hoverElevation;
final double highlightElevation;
final Widget child;
final T initialValue;
final List<PopupMenuEntry<T>> items;
final ValueChanged<T> onSelected;
@override @override
_QuillDropdownButtonState<T> createState() => _QuillDropdownButtonState<T>(); _QuillDropdownButtonState<T> createState() => _QuillDropdownButtonState<T>();
} }
@ -1251,7 +1251,7 @@ class _QuillDropdownButtonState<T> extends State<QuillDropdownButton<T>> {
// widget.shape ?? popupMenuTheme.shape, // widget.shape ?? popupMenuTheme.shape,
color: popupMenuTheme.color, // widget.color ?? popupMenuTheme.color, color: popupMenuTheme.color, // widget.color ?? popupMenuTheme.color,
// captureInheritedThemes: widget.captureInheritedThemes, // captureInheritedThemes: widget.captureInheritedThemes,
).then((T? newValue) { ).then((newValue) {
if (!mounted) return null; if (!mounted) return null;
if (newValue == null) { if (newValue == null) {
// if (widget.onCanceled != null) widget.onCanceled(); // if (widget.onCanceled != null) widget.onCanceled();
@ -1265,7 +1265,7 @@ class _QuillDropdownButtonState<T> extends State<QuillDropdownButton<T>> {
return ConstrainedBox( return ConstrainedBox(
constraints: const BoxConstraints.tightFor(width: 110), constraints: const BoxConstraints.tightFor(width: 110),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row( child: Row(
children: [ children: [
widget.child, widget.child,

@ -1,6 +1,6 @@
name: flutter_quill name: flutter_quill
description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us)
version: 1.1.7 version: 1.1.8
#author: bulletjournal #author: bulletjournal
homepage: https://bulletjournal.us/home/index.html homepage: https://bulletjournal.us/home/index.html
repository: https://github.com/singerdmx/flutter-quill repository: https://github.com/singerdmx/flutter-quill

Loading…
Cancel
Save