Browse Source

Merge pull request #63 from memspace/zefyr-view

Zefyr view
Anatoly Pulyaevskiy 6 years ago
parent
commit
8e3e78653b
No account linked to committer's email address
42 changed files with 791 additions and 522 deletions
  1. 1
    1
      doc/quick_start.md
  2. 4
    3
      packages/notus/analysis_options.yaml
  3. 4
    4
      packages/notus/lib/src/convert/markdown.dart
  4. 1
    1
      packages/notus/lib/src/document.dart
  5. 9
    9
      packages/notus/lib/src/document/attributes.dart
  6. 1
    1
      packages/notus/lib/src/document/block.dart
  7. 2
    2
      packages/notus/lib/src/document/leaf.dart
  8. 5
    3
      packages/notus/lib/src/document/line.dart
  9. 1
    1
      packages/notus/lib/src/document/node.dart
  10. 2
    1
      packages/notus/lib/src/heuristics/delete_rules.dart
  11. 3
    3
      packages/notus/lib/src/heuristics/insert_rules.dart
  12. 1
    1
      packages/notus/test/convert/markdown_test.dart
  13. 1
    1
      packages/notus/test/document/attributes_test.dart
  14. 1
    1
      packages/notus/test/document_test.dart
  15. 13
    0
      packages/zefyr/CHANGELOG.md
  16. 4
    3
      packages/zefyr/analysis_options.yaml
  17. 12
    0
      packages/zefyr/example/lib/main.dart
  18. 17
    1
      packages/zefyr/example/lib/src/full_page.dart
  19. 87
    0
      packages/zefyr/example/lib/src/view.dart
  20. 9
    9
      packages/zefyr/lib/src/widgets/buttons.dart
  21. 24
    41
      packages/zefyr/lib/src/widgets/common.dart
  22. 42
    0
      packages/zefyr/lib/src/widgets/cursor_timer.dart
  23. 16
    5
      packages/zefyr/lib/src/widgets/editable_box.dart
  24. 29
    98
      packages/zefyr/lib/src/widgets/editable_text.dart
  25. 13
    167
      packages/zefyr/lib/src/widgets/editor.dart
  26. 1
    1
      packages/zefyr/lib/src/widgets/image.dart
  27. 2
    2
      packages/zefyr/lib/src/widgets/quote.dart
  28. 3
    2
      packages/zefyr/lib/src/widgets/render_context.dart
  29. 8
    7
      packages/zefyr/lib/src/widgets/rich_text.dart
  30. 232
    0
      packages/zefyr/lib/src/widgets/scope.dart
  31. 41
    28
      packages/zefyr/lib/src/widgets/selection.dart
  32. 3
    3
      packages/zefyr/lib/src/widgets/toolbar.dart
  33. 105
    0
      packages/zefyr/lib/src/widgets/view.dart
  34. 2
    0
      packages/zefyr/lib/zefyr.dart
  35. 4
    4
      packages/zefyr/test/rendering/render_editable_paragraph_test.dart
  36. 2
    2
      packages/zefyr/test/widgets/buttons_test.dart
  37. 0
    107
      packages/zefyr/test/widgets/editable_text_scope_test.dart
  38. 1
    1
      packages/zefyr/test/widgets/editor_test.dart
  39. 1
    0
      packages/zefyr/test/widgets/image_test.dart
  40. 5
    5
      packages/zefyr/test/widgets/rich_text_test.dart
  41. 75
    0
      packages/zefyr/test/widgets/scope_test.dart
  42. 4
    4
      packages/zefyr/test/widgets/selection_test.dart

+ 1
- 1
doc/quick_start.md View File

@@ -12,7 +12,7 @@ Add `zefyr` package as a dependency to your `pubspec.yaml`:
12 12
 
13 13
 ```yaml
14 14
 dependencies:
15
-  zefyr: ^0.3.0
15
+  zefyr: [latest_version]
16 16
 ```
17 17
 
18 18
 And run `flutter packages get` to install. This installs both `zefyr`

+ 4
- 3
packages/notus/analysis_options.yaml View File

@@ -1,11 +1,12 @@
1 1
 analyzer:
2
-  language:
3
-    enableSuperMixins: true
2
+  strong-mode:
3
+    implicit-casts: false
4
+    implicit-dynamic: false
4 5
 
5 6
 # Lint rules and documentation, see http://dart-lang.github.io/linter/lints
6 7
 linter:
7 8
   rules:
8
-    # - avoid_init_to_null
9
+    - avoid_init_to_null
9 10
     - cancel_subscriptions
10 11
     - close_sinks
11 12
     - directives_ordering

+ 4
- 4
packages/notus/lib/src/convert/markdown.dart View File

@@ -114,7 +114,7 @@ class _NotusMarkdownEncoder extends Converter<Delta, String> {
114 114
   String _writeLine(String text, NotusStyle style) {
115 115
     StringBuffer buffer = new StringBuffer();
116 116
     if (style.contains(NotusAttribute.heading)) {
117
-      _writeAttribute(buffer, style.get(NotusAttribute.heading));
117
+      _writeAttribute(buffer, style.get<int>(NotusAttribute.heading));
118 118
     }
119 119
 
120 120
     // Write the text itself
@@ -163,11 +163,11 @@ class _NotusMarkdownEncoder extends Converter<Delta, String> {
163 163
     } else if (attribute == NotusAttribute.italic) {
164 164
       _writeItalicTag(buffer);
165 165
     } else if (attribute.key == NotusAttribute.link.key) {
166
-      _writeLinkTag(buffer, attribute, close: close);
166
+      _writeLinkTag(buffer, attribute as NotusAttribute<String>, close: close);
167 167
     } else if (attribute.key == NotusAttribute.heading.key) {
168
-      _writeHeadingTag(buffer, attribute);
168
+      _writeHeadingTag(buffer, attribute as NotusAttribute<int>);
169 169
     } else if (attribute.key == NotusAttribute.block.key) {
170
-      _writeBlockTag(buffer, attribute, close: close);
170
+      _writeBlockTag(buffer, attribute as NotusAttribute<String>, close: close);
171 171
     } else {
172 172
       throw new ArgumentError('Cannot handle $attribute');
173 173
     }

+ 1
- 1
packages/notus/lib/src/document.dart View File

@@ -44,7 +44,7 @@ class NotusDocument {
44 44
     _loadDocument(_delta);
45 45
   }
46 46
 
47
-  NotusDocument.fromJson(dynamic data)
47
+  NotusDocument.fromJson(List data)
48 48
       : _heuristics = NotusHeuristics.fallback,
49 49
         _delta = Delta.fromJson(data) {
50 50
     _loadDocument(_delta);

+ 9
- 9
packages/notus/lib/src/document/attributes.dart View File

@@ -122,7 +122,7 @@ class NotusAttribute<T> implements NotusAttributeBuilder<T> {
122 122
   /// Embed style attribute.
123 123
   static const embed = const EmbedAttributeBuilder._();
124 124
 
125
-  factory NotusAttribute._fromKeyValue(String key, T value) {
125
+  static NotusAttribute _fromKeyValue(String key, dynamic value) {
126 126
     if (!_registry.containsKey(key))
127 127
       throw new ArgumentError.value(
128 128
           key, 'No attribute with key "$key" registered.');
@@ -166,7 +166,7 @@ class NotusAttribute<T> implements NotusAttributeBuilder<T> {
166 166
       new NotusAttribute<T>._(key, scope, value);
167 167
 
168 168
   @override
169
-  bool operator ==(other) {
169
+  bool operator ==(Object other) {
170 170
     if (identical(this, other)) return true;
171 171
     if (other is! NotusAttribute<T>) return false;
172 172
     NotusAttribute<T> typedOther = other;
@@ -190,11 +190,11 @@ class NotusStyle {
190 190
 
191 191
   final Map<String, NotusAttribute> _data;
192 192
 
193
-  static NotusStyle fromJson(Map data) {
193
+  static NotusStyle fromJson(Map<String, dynamic> data) {
194 194
     if (data == null) return new NotusStyle();
195 195
 
196
-    final result = data.map((key, value) {
197
-      var attr = new NotusAttribute._fromKeyValue(key, value);
196
+    final result = data.map((String key, dynamic value) {
197
+      var attr = NotusAttribute._fromKeyValue(key, value);
198 198
       return new MapEntry<String, NotusAttribute>(key, attr);
199 199
     });
200 200
     return new NotusStyle._(result);
@@ -228,7 +228,7 @@ class NotusStyle {
228 228
   /// [attribute].
229 229
   bool containsSame(NotusAttribute attribute) {
230 230
     assert(attribute != null);
231
-    return get(attribute) == attribute;
231
+    return get<dynamic>(attribute) == attribute;
232 232
   }
233 233
 
234 234
   /// Returns value of specified attribute [key] in this set.
@@ -290,11 +290,11 @@ class NotusStyle {
290 290
   /// Returns JSON-serializable representation of this style.
291 291
   Map<String, dynamic> toJson() => _data.isEmpty
292 292
       ? null
293
-      : _data.map(
294
-          (_, value) => new MapEntry<String, dynamic>(value.key, value.value));
293
+      : _data.map<String, dynamic>((String _, NotusAttribute value) =>
294
+          new MapEntry<String, dynamic>(value.key, value.value));
295 295
 
296 296
   @override
297
-  bool operator ==(other) {
297
+  bool operator ==(Object other) {
298 298
     if (identical(this, other)) return true;
299 299
     if (other is! NotusStyle) return false;
300 300
     NotusStyle typedOther = other;

+ 1
- 1
packages/notus/lib/src/document/block.dart View File

@@ -40,7 +40,7 @@ class BlockNode extends ContainerNode<LineNode>
40 40
       while (child != line) {
41 41
         child.unlink();
42 42
         before.add(child);
43
-        child = this.first;
43
+        child = this.first as LineNode;
44 44
       }
45 45
       line.unlink();
46 46
       insertBefore(line);

+ 2
- 2
packages/notus/lib/src/document/leaf.dart View File

@@ -52,7 +52,7 @@ abstract class LeafNode extends Node
52 52
     assert(index >= 0 && index <= length);
53 53
     if (index == 0) return this;
54 54
     if (index == length && isLast) return null;
55
-    if (index == length && !isLast) return next;
55
+    if (index == length && !isLast) return next as LeafNode;
56 56
 
57 57
     String text = _value;
58 58
     _value = text.substring(0, index);
@@ -124,7 +124,7 @@ abstract class LeafNode extends Node
124 124
   }
125 125
 
126 126
   @override
127
-  LineNode get parent => super.parent;
127
+  LineNode get parent => super.parent as LineNode;
128 128
 
129 129
   @override
130 130
   int get length => _value.length;

+ 5
- 3
packages/notus/lib/src/document/line.dart View File

@@ -34,13 +34,15 @@ class LineNode extends ContainerNode<LeafNode>
34 34
     if (isLast) {
35 35
       if (parent is BlockNode) {
36 36
         if (parent.isLast) return null;
37
-        return (parent.next is BlockNode)
37
+        LineNode line = (parent.next is BlockNode)
38 38
             ? (parent.next as BlockNode).first
39 39
             : parent.next;
40
+        return line;
40 41
       } else
41 42
         return null;
42 43
     } else {
43
-      return (next is BlockNode) ? (next as BlockNode).first : next;
44
+      LineNode line = (next is BlockNode) ? (next as BlockNode).first : next;
45
+      return line;
44 46
     }
45 47
   }
46 48
 
@@ -129,7 +131,7 @@ class LineNode extends ContainerNode<LeafNode>
129 131
       result = result.mergeAll(node.style);
130 132
       int pos = node.length - data.offset;
131 133
       while (!node.isLast && pos < local) {
132
-        node = node.next;
134
+        node = node.next as LeafNode;
133 135
         _handle(node.style);
134 136
         pos += node.length;
135 137
       }

+ 1
- 1
packages/notus/lib/src/document/node.dart View File

@@ -238,7 +238,7 @@ abstract class ContainerNode<T extends Node> extends Node {
238 238
 
239 239
     if (isEmpty) {
240 240
       assert(index == 0);
241
-      Node node = defaultChild;
241
+      T node = defaultChild;
242 242
       add(node);
243 243
       node.insert(index, value, style);
244 244
     } else {

+ 2
- 1
packages/notus/lib/src/heuristics/delete_rules.dart View File

@@ -69,7 +69,8 @@ class PreserveLineStyleOnMergeRule extends DeleteRule {
69 69
 
70 70
   Map<String, dynamic> _unsetAttributes(Map<String, dynamic> attributes) {
71 71
     if (attributes == null) return null;
72
-    return attributes.map((key, value) => new MapEntry(key, null));
72
+    return attributes.map<String, dynamic>((String key, dynamic value) =>
73
+        new MapEntry<String, dynamic>(key, null));
73 74
   }
74 75
 }
75 76
 

+ 3
- 3
packages/notus/lib/src/heuristics/insert_rules.dart View File

@@ -87,7 +87,7 @@ class ResetLineFormatOnNewLineRule extends InsertRule {
87 87
     final target = iter.next();
88 88
 
89 89
     if (target.data.startsWith('\n')) {
90
-      Map<String, dynamic> resetStyle = null;
90
+      Map<String, dynamic> resetStyle;
91 91
       if (target.attributes != null &&
92 92
           target.attributes.containsKey(NotusAttribute.heading.key)) {
93 93
         resetStyle = NotusAttribute.heading.unset.toJson();
@@ -290,8 +290,8 @@ class PreserveBlockStyleOnPasteRule extends InsertRule {
290 290
       }
291 291
     }
292 292
 
293
-    Map<String, dynamic> resetStyle = null;
294
-    Map<String, dynamic> blockStyle = null;
293
+    Map<String, dynamic> resetStyle;
294
+    Map<String, dynamic> blockStyle;
295 295
     if (lineStyle != null) {
296 296
       if (lineStyle.containsKey(NotusAttribute.heading.key)) {
297 297
         resetStyle = NotusAttribute.heading.unset.toJson();

+ 1
- 1
packages/notus/test/convert/markdown_test.dart View File

@@ -141,7 +141,7 @@ void main() {
141 141
 
142 142
 final doc =
143 143
     r'[{"insert":"Zefyr"},{"insert":"\n","attributes":{"heading":1}},{"insert":"Soft and gentle rich text editing for Flutter applications.","attributes":{"i":true}},{"insert":"\nZefyr is an "},{"insert":"early preview","attributes":{"b":true}},{"insert":" open source library.\nDocumentation"},{"insert":"\n","attributes":{"heading":3}},{"insert":"Quick Start"},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Data format and Document Model"},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Style attributes"},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Heuristic rules"},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Clean and modern look"},{"insert":"\n","attributes":{"heading":2}},{"insert":"Zefyr’s rich text editor is built with simplicity and flexibility in mind. It provides clean interface for distraction-free editing. Think Medium.com-like experience.\nimport ‘package:flutter/material.dart’;"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"import ‘package:notus/notus.dart’;"},{"insert":"\n\n","attributes":{"block":"code"}},{"insert":"void main() {"},{"insert":"\n","attributes":{"block":"code"}},{"insert":" print(“Hello world!”);"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"}"},{"insert":"\n","attributes":{"block":"code"}}]';
144
-final delta = Delta.fromJson(json.decode(doc));
144
+final delta = Delta.fromJson(json.decode(doc) as List);
145 145
 
146 146
 final expectedMarkdown = '''
147 147
 # Zefyr

+ 1
- 1
packages/notus/test/document/attributes_test.dart View File

@@ -7,7 +7,7 @@ import 'package:notus/notus.dart';
7 7
 void main() {
8 8
   group('$NotusStyle', () {
9 9
     test('get', () {
10
-      var attrs = NotusStyle.fromJson({'block': 'ul'});
10
+      var attrs = NotusStyle.fromJson(<String, dynamic>{'block': 'ul'});
11 11
       var attr = attrs.get(NotusAttribute.block);
12 12
       expect(attr, NotusAttribute.ul);
13 13
     });

+ 1
- 1
packages/notus/test/document_test.dart View File

@@ -46,7 +46,7 @@ void main() {
46 46
     test('json serialization', () {
47 47
       final original = dartconfDoc();
48 48
       final jsonData = json.encode(original);
49
-      final doc = NotusDocument.fromJson(json.decode(jsonData));
49
+      final doc = NotusDocument.fromJson(json.decode(jsonData) as List);
50 50
       expect(doc.toDelta(), original.toDelta());
51 51
       expect(json.encode(doc), jsonData);
52 52
     });

+ 13
- 0
packages/zefyr/CHANGELOG.md View File

@@ -1,3 +1,16 @@
1
+## Unreleased
2
+
3
+* Added non-scrollable `ZefyrView` widget which allows previewing Notus documents inside
4
+  layouts using their own scrollables like ListView.
5
+* Breaking change: renamed `EditableRichText` to `ZefyrRichText`. User code is unlikely to be
6
+  affected unless you've extended Zefyr with custom implementations of block widgets.
7
+* Breaking change: renamed `RenderEditableParagraph` to `RenderZefyrParagraph`. User code is
8
+  unlikely to be affected unless you've extended Zefyr with custom implementations of block widgets.
9
+* Added `ZefyrScope` class - replaces previously used scope objects `ZefyrEditableTextScope` and
10
+  `ZefyrEditorScope`. Unified all shared resources under one class.
11
+* Breaking change: removed `ZefyrEditor.of` and `ZefyrEditableText.of` static methods.
12
+  Use `ZefyrScope.of` instead.
13
+
1 14
 ## 0.3.0
2 15
 
3 16
 This version introduces new widget `ZefyrScaffold` which allows embedding Zefyr in custom

+ 4
- 3
packages/zefyr/analysis_options.yaml View File

@@ -1,11 +1,12 @@
1 1
 analyzer:
2
-  language:
3
-#    enableSuperMixins: true
2
+  strong-mode:
3
+    implicit-casts: false
4
+    implicit-dynamic: false
4 5
 
5 6
 # Lint rules and documentation, see http://dart-lang.github.io/linter/lints
6 7
 linter:
7 8
   rules:
8
-    # - avoid_init_to_null
9
+    - avoid_init_to_null
9 10
     - cancel_subscriptions
10 11
     - close_sinks
11 12
     - directives_ordering

+ 12
- 0
packages/zefyr/example/lib/main.dart View File

@@ -4,6 +4,7 @@
4 4
 import 'package:flutter/material.dart';
5 5
 import 'src/form.dart';
6 6
 import 'src/full_page.dart';
7
+import 'src/view.dart';
7 8
 
8 9
 void main() {
9 10
   runApp(new ZefyrApp());
@@ -20,6 +21,7 @@ class ZefyrApp extends StatelessWidget {
20 21
       routes: {
21 22
         "/fullPage": buildFullPage,
22 23
         "/form": buildFormPage,
24
+        "/view": buildViewPage,
23 25
       },
24 26
     );
25 27
   }
@@ -31,6 +33,10 @@ class ZefyrApp extends StatelessWidget {
31 33
   Widget buildFormPage(BuildContext context) {
32 34
     return FormEmbeddedScreen();
33 35
   }
36
+
37
+  Widget buildViewPage(BuildContext context) {
38
+    return ViewScreen();
39
+  }
34 40
 }
35 41
 
36 42
 class HomePage extends StatelessWidget {
@@ -59,6 +65,12 @@ class HomePage extends StatelessWidget {
59 65
             color: Colors.lightBlue,
60 66
             textColor: Colors.white,
61 67
           ),
68
+          FlatButton(
69
+            onPressed: () => nav.pushNamed('/view'),
70
+            child: Text('Read-only embeddable view'),
71
+            color: Colors.lightBlue,
72
+            textColor: Colors.white,
73
+          ),
62 74
           Expanded(child: Container()),
63 75
         ],
64 76
       ),

+ 17
- 1
packages/zefyr/example/lib/src/full_page.dart View File

@@ -1,3 +1,4 @@
1
+import 'dart:async';
1 2
 import 'dart:convert';
2 3
 
3 4
 import 'package:flutter/material.dart';
@@ -31,7 +32,7 @@ final doc =
31 32
     r'g":2}},{"insert":"Of course:\nimport ‘package:flutter/material.dart’;"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"import ‘package:zefyr/zefyr.dart’;"},{"insert":"\n\n","attributes":{"block":"code"}},{"insert":"void main() {"},{"insert":"\n","attributes":{"block":"code"}},{"insert":" runApp(MyZefyrApp());"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"}"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"\n\n\n"}]';
32 33
 
33 34
 Delta getDelta() {
34
-  return Delta.fromJson(json.decode(doc));
35
+  return Delta.fromJson(json.decode(doc) as List);
35 36
 }
36 37
 
37 38
 class _FullPageEditorScreenState extends State<FullPageEditorScreen> {
@@ -39,6 +40,21 @@ class _FullPageEditorScreenState extends State<FullPageEditorScreen> {
39 40
       ZefyrController(NotusDocument.fromDelta(getDelta()));
40 41
   final FocusNode _focusNode = new FocusNode();
41 42
   bool _editing = false;
43
+  StreamSubscription<NotusChange> _sub;
44
+
45
+  @override
46
+  void initState() {
47
+    super.initState();
48
+    _sub = _controller.document.changes.listen((change) {
49
+      print('${change.source}: ${change.change}');
50
+    });
51
+  }
52
+
53
+  @override
54
+  void dispose() {
55
+    _sub.cancel();
56
+    super.dispose();
57
+  }
42 58
 
43 59
   @override
44 60
   Widget build(BuildContext context) {

+ 87
- 0
packages/zefyr/example/lib/src/view.dart View File

@@ -0,0 +1,87 @@
1
+import 'dart:convert';
2
+
3
+import 'package:flutter/material.dart';
4
+import 'package:quill_delta/quill_delta.dart';
5
+import 'package:zefyr/zefyr.dart';
6
+
7
+import 'full_page.dart';
8
+
9
+class ViewScreen extends StatefulWidget {
10
+  @override
11
+  _ViewScreen createState() => new _ViewScreen();
12
+}
13
+
14
+final doc =
15
+    r'[{"insert":"Zefyr"},{"insert":"\n","attributes":{"heading":1}},{"insert":"Soft and gentle rich text editing for Flutter applications.","attributes":{"i":true}},{"insert":"\n"},{"insert":"​","attributes":{"embed":{"type":"image","source":"asset://images/breeze.jpg"}}},{"insert":"\n"},{"insert":"Photo by Hiroyuki Takeda.","attributes":{"i":true}},{"insert":"\nZefyr is currently in "},{"insert":"early preview","attributes":{"b":true}},{"insert":". If you have a feature request or found a bug, please file it at the "},{"insert":"issue tracker","attributes":{"a":"https://github.com/memspace/zefyr/issues"}},{"insert":'
16
+    r'".\nDocumentation"},{"insert":"\n","attributes":{"heading":3}},{"insert":"Quick Start","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/quick_start.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Data Format and Document Model","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/data_and_document.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Style Attributes","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/attr'
17
+    r'ibutes.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Heuristic Rules","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/heuristics.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"FAQ","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/faq.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Clean and modern look"},{"insert":"\n","attributes":{"heading":2}},{"insert":"Zefyr’s rich text editor is built with simplicity and fle'
18
+    r'xibility in mind. It provides clean interface for distraction-free editing. Think Medium.com-like experience.\nMarkdown inspired semantics"},{"insert":"\n","attributes":{"heading":2}},{"insert":"Ever needed to have a heading line inside of a quote block, like this:\nI’m a Markdown heading"},{"insert":"\n","attributes":{"block":"quote","heading":3}},{"insert":"And I’m a regular paragraph"},{"insert":"\n","attributes":{"block":"quote"}},{"insert":"Code blocks"},{"insert":"\n","attributes":{"headin'
19
+    r'g":2}},{"insert":"Of course:\nimport ‘package:flutter/material.dart’;"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"import ‘package:zefyr/zefyr.dart’;"},{"insert":"\n\n","attributes":{"block":"code"}},{"insert":"void main() {"},{"insert":"\n","attributes":{"block":"code"}},{"insert":" runApp(MyZefyrApp());"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"}"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"\n\n\n"}]';
20
+
21
+Delta getDelta() {
22
+  return Delta.fromJson(json.decode(doc) as List);
23
+}
24
+
25
+class _ViewScreen extends State<ViewScreen> {
26
+  final doc = NotusDocument.fromDelta(getDelta());
27
+
28
+  @override
29
+  Widget build(BuildContext context) {
30
+    final theme = new ZefyrThemeData(
31
+      toolbarTheme: ZefyrToolbarTheme.fallback(context).copyWith(
32
+        color: Colors.grey.shade800,
33
+        toggleColor: Colors.grey.shade900,
34
+        iconColor: Colors.white,
35
+        disabledIconColor: Colors.grey.shade500,
36
+      ),
37
+    );
38
+    return Scaffold(
39
+      resizeToAvoidBottomPadding: true,
40
+      appBar: AppBar(
41
+        elevation: 1.0,
42
+        backgroundColor: Colors.grey.shade200,
43
+        brightness: Brightness.light,
44
+        title: ZefyrLogo(),
45
+      ),
46
+      body: ZefyrTheme(
47
+        data: theme,
48
+        child: ListView(
49
+          children: <Widget>[
50
+            SizedBox(height: 16.0),
51
+            ListTile(
52
+              leading: Icon(Icons.info),
53
+              title: Text('ZefyrView inside ListView'),
54
+              subtitle: Text(
55
+                  'Allows embedding Notus documents in custom scrollables'),
56
+              trailing: Icon(Icons.keyboard_arrow_down),
57
+            ),
58
+            Padding(
59
+              padding: const EdgeInsets.all(16.0),
60
+              child: ZefyrView(
61
+                document: doc,
62
+                imageDelegate: new CustomImageDelegate(),
63
+              ),
64
+            )
65
+          ],
66
+        ),
67
+      ),
68
+    );
69
+  }
70
+}
71
+
72
+/// Custom image delegate used by this example to load image from application
73
+/// assets.
74
+///
75
+/// Default image delegate only supports [FileImage]s.
76
+class CustomImageDelegate extends ZefyrDefaultImageDelegate {
77
+  @override
78
+  Widget buildImage(BuildContext context, String imageSource) {
79
+    // We use custom "asset" scheme to distinguish asset images from other files.
80
+    if (imageSource.startsWith('asset://')) {
81
+      final asset = new AssetImage(imageSource.replaceFirst('asset://', ''));
82
+      return new Image(image: asset);
83
+    } else {
84
+      return super.buildImage(context, imageSource);
85
+    }
86
+  }
87
+}

+ 9
- 9
packages/zefyr/lib/src/widgets/buttons.dart View File

@@ -7,7 +7,7 @@ import 'package:image_picker/image_picker.dart';
7 7
 import 'package:notus/notus.dart';
8 8
 import 'package:url_launcher/url_launcher.dart';
9 9
 
10
-import 'editor.dart';
10
+import 'scope.dart';
11 11
 import 'theme.dart';
12 12
 import 'toolbar.dart';
13 13
 
@@ -94,7 +94,7 @@ class ZefyrButton extends StatelessWidget {
94 94
     }
95 95
   }
96 96
 
97
-  Color _getColor(ZefyrEditorScope editor, ZefyrToolbarTheme theme) {
97
+  Color _getColor(ZefyrScope editor, ZefyrToolbarTheme theme) {
98 98
     if (isAttributeAction) {
99 99
       final attribute = kZefyrToolbarAttributeActions[action];
100 100
       final isToggled = (attribute is NotusAttribute)
@@ -106,7 +106,7 @@ class ZefyrButton extends StatelessWidget {
106 106
   }
107 107
 
108 108
   VoidCallback _getPressedHandler(
109
-      ZefyrEditorScope editor, ZefyrToolbarState toolbar) {
109
+      ZefyrScope editor, ZefyrToolbarState toolbar) {
110 110
     if (onPressed != null) {
111 111
       return onPressed;
112 112
     } else if (isAttributeAction) {
@@ -123,7 +123,7 @@ class ZefyrButton extends StatelessWidget {
123 123
     return null;
124 124
   }
125 125
 
126
-  void _toggleAttribute(NotusAttribute attribute, ZefyrEditorScope editor) {
126
+  void _toggleAttribute(NotusAttribute attribute, ZefyrScope editor) {
127 127
     final isToggled = editor.selectionStyle.containsSame(attribute);
128 128
     if (isToggled) {
129 129
       editor.formatSelection(attribute.unset);
@@ -303,7 +303,7 @@ class _LinkButtonState extends State<LinkButton> {
303 303
   final TextEditingController _inputController = TextEditingController();
304 304
   Key _inputKey;
305 305
   bool _formatError = false;
306
-  ZefyrEditorScope _editor;
306
+  ZefyrScope _editor;
307 307
 
308 308
   bool get isEditing => _inputKey != null;
309 309
 
@@ -491,7 +491,7 @@ class _LinkInput extends StatefulWidget {
491 491
 class _LinkInputState extends State<_LinkInput> {
492 492
   final FocusNode _focusNode = FocusNode();
493 493
 
494
-  ZefyrEditorScope _editor;
494
+  ZefyrScope _editor;
495 495
   bool _didAutoFocus = false;
496 496
 
497 497
   @override
@@ -505,15 +505,15 @@ class _LinkInputState extends State<_LinkInput> {
505 505
     final toolbar = ZefyrToolbar.of(context);
506 506
 
507 507
     if (_editor != toolbar.editor) {
508
-      _editor?.setToolbarFocusNode(null);
508
+      _editor?.toolbarFocusNode = null;
509 509
       _editor = toolbar.editor;
510
-      _editor.setToolbarFocusNode(_focusNode);
510
+      _editor.toolbarFocusNode = _focusNode;
511 511
     }
512 512
   }
513 513
 
514 514
   @override
515 515
   void dispose() {
516
-    _editor?.setToolbarFocusNode(null);
516
+    _editor?.toolbarFocusNode = null;
517 517
     _focusNode.dispose();
518 518
     _editor = null;
519 519
     super.dispose();

+ 24
- 41
packages/zefyr/lib/src/widgets/common.dart View File

@@ -6,10 +6,10 @@ import 'package:flutter/widgets.dart';
6 6
 import 'package:notus/notus.dart';
7 7
 
8 8
 import 'editable_box.dart';
9
-import 'editable_text.dart';
10 9
 import 'horizontal_rule.dart';
11 10
 import 'image.dart';
12 11
 import 'rich_text.dart';
12
+import 'scope.dart';
13 13
 import 'theme.dart';
14 14
 
15 15
 /// Raw widget representing a single line of rich text document in Zefyr editor.
@@ -43,42 +43,45 @@ class _RawZefyrLineState extends State<RawZefyrLine> {
43 43
 
44 44
   @override
45 45
   Widget build(BuildContext context) {
46
-    ensureVisible(context);
46
+    final scope = ZefyrScope.of(context);
47
+    if (scope.isEditable) {
48
+      ensureVisible(context, scope);
49
+    }
47 50
     final theme = ZefyrTheme.of(context);
48
-    final editable = ZefyrEditableText.of(context);
49 51
 
50 52
     Widget content;
51 53
     if (widget.node.hasEmbed) {
52
-      content = buildEmbed(context);
54
+      content = buildEmbed(context, scope);
53 55
     } else {
54 56
       assert(widget.style != null);
55
-
56
-      final text = new EditableRichText(
57
+      content = ZefyrRichText(
57 58
         node: widget.node,
58 59
         text: buildText(context),
59 60
       );
60
-      content = new EditableBox(
61
-        child: text,
61
+    }
62
+
63
+    if (scope.isEditable) {
64
+      content = EditableBox(
65
+        child: content,
62 66
         node: widget.node,
63 67
         layerLink: _link,
64
-        renderContext: editable.renderContext,
65
-        showCursor: editable.showCursor,
66
-        selection: editable.selection,
68
+        renderContext: scope.renderContext,
69
+        showCursor: scope.showCursor,
70
+        selection: scope.selection,
67 71
         selectionColor: theme.selectionColor,
68 72
       );
73
+      content = CompositedTransformTarget(link: _link, child: content);
69 74
     }
70 75
 
71
-    final result = new CompositedTransformTarget(link: _link, child: content);
72 76
     if (widget.padding != null) {
73
-      return new Padding(padding: widget.padding, child: result);
77
+      return Padding(padding: widget.padding, child: content);
74 78
     }
75
-    return result;
79
+    return content;
76 80
   }
77 81
 
78
-  void ensureVisible(BuildContext context) {
79
-    final editable = ZefyrEditableText.of(context);
80
-    if (editable.selection.isCollapsed &&
81
-        widget.node.containsOffset(editable.selection.extentOffset)) {
82
+  void ensureVisible(BuildContext context, ZefyrScope scope) {
83
+    if (scope.selection.isCollapsed &&
84
+        widget.node.containsOffset(scope.selection.extentOffset)) {
82 85
       WidgetsBinding.instance.addPostFrameCallback((_) {
83 86
         bringIntoView(context);
84 87
       });
@@ -136,34 +139,14 @@ class _RawZefyrLineState extends State<RawZefyrLine> {
136 139
     return result;
137 140
   }
138 141
 
139
-  Widget buildEmbed(BuildContext context) {
140
-    final theme = ZefyrTheme.of(context);
141
-    final editable = ZefyrEditableText.of(context);
142
-
142
+  Widget buildEmbed(BuildContext context, ZefyrScope scope) {
143 143
     EmbedNode node = widget.node.children.single;
144 144
     EmbedAttribute embed = node.style.get(NotusAttribute.embed);
145 145
 
146 146
     if (embed.type == EmbedType.horizontalRule) {
147
-      final hr = new ZefyrHorizontalRule(node: node);
148
-      return new EditableBox(
149
-        child: hr,
150
-        node: widget.node,
151
-        layerLink: _link,
152
-        renderContext: editable.renderContext,
153
-        showCursor: editable.showCursor,
154
-        selection: editable.selection,
155
-        selectionColor: theme.selectionColor,
156
-      );
147
+      return ZefyrHorizontalRule(node: node);
157 148
     } else if (embed.type == EmbedType.image) {
158
-      return new EditableBox(
159
-        child: ZefyrImage(node: node, delegate: editable.imageDelegate),
160
-        node: widget.node,
161
-        layerLink: _link,
162
-        renderContext: editable.renderContext,
163
-        showCursor: editable.showCursor,
164
-        selection: editable.selection,
165
-        selectionColor: theme.selectionColor,
166
-      );
149
+      return ZefyrImage(node: node, delegate: scope.imageDelegate);
167 150
     } else {
168 151
       throw new UnimplementedError('Unimplemented embed type ${embed.type}');
169 152
     }

+ 42
- 0
packages/zefyr/lib/src/widgets/cursor_timer.dart View File

@@ -0,0 +1,42 @@
1
+import 'dart:async';
2
+
3
+import 'package:flutter/material.dart';
4
+
5
+/// Helper class that keeps state relevant to the editing cursor.
6
+class CursorTimer {
7
+  static const _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500);
8
+
9
+  Timer _timer;
10
+  final ValueNotifier<bool> _showCursor = new ValueNotifier<bool>(false);
11
+
12
+  ValueNotifier<bool> get value => _showCursor;
13
+
14
+  void _cursorTick(Timer timer) {
15
+    _showCursor.value = !_showCursor.value;
16
+  }
17
+
18
+  /// Starts cursor timer.
19
+  void start() {
20
+    _showCursor.value = true;
21
+    _timer = new Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick);
22
+  }
23
+
24
+  /// Stops cursor timer.
25
+  void stop() {
26
+    _timer?.cancel();
27
+    _timer = null;
28
+    _showCursor.value = false;
29
+  }
30
+
31
+  /// Starts or stops cursor timer based on current state of [focusNode]
32
+  /// and [selection].
33
+  void startOrStop(FocusNode focusNode, TextSelection selection) {
34
+    final hasFocus = focusNode.hasFocus;
35
+    final selectionCollapsed = selection.isCollapsed;
36
+    if (_timer == null && hasFocus && selectionCollapsed) {
37
+      start();
38
+    } else if (_timer != null && (!hasFocus || !selectionCollapsed)) {
39
+      stop();
40
+    }
41
+  }
42
+}

+ 16
- 5
packages/zefyr/lib/src/widgets/editable_box.dart View File

@@ -102,9 +102,9 @@ class RenderEditableProxyBox extends RenderBox
102 102
   set showCursor(ValueNotifier<bool> value) {
103 103
     assert(value != null);
104 104
     if (_showCursor == value) return;
105
-    if (attached) _showCursor.removeListener(markNeedsPaint);
105
+    if (attached) _showCursor.removeListener(markNeedsCursorPaint);
106 106
     _showCursor = value;
107
-    if (attached) _showCursor.addListener(markNeedsPaint);
107
+    if (attached) _showCursor.addListener(markNeedsCursorPaint);
108 108
     markNeedsPaint();
109 109
   }
110 110
 
@@ -130,8 +130,13 @@ class RenderEditableProxyBox extends RenderBox
130 130
   /// Returns `true` if current selection is collapsed, located within
131 131
   /// this paragraph and is visible according to tick timer.
132 132
   bool get isCaretVisible {
133
+    return _showCursor.value && containsCaret;
134
+  }
135
+
136
+  /// Returns `true` if current selection is collapsed and located
137
+  /// within this paragraph.
138
+  bool get containsCaret {
133 139
     if (!_selection.isCollapsed) return false;
134
-    if (!_showCursor.value) return false;
135 140
 
136 141
     final int start = node.documentOffset;
137 142
     final int end = start + node.length;
@@ -146,6 +151,12 @@ class RenderEditableProxyBox extends RenderBox
146 151
     return intersectsWithSelection(_selection);
147 152
   }
148 153
 
154
+  void markNeedsCursorPaint() {
155
+    if (containsCaret) {
156
+      markNeedsPaint();
157
+    }
158
+  }
159
+
149 160
   //
150 161
   // Overridden members of RenderBox
151 162
   //
@@ -153,13 +164,13 @@ class RenderEditableProxyBox extends RenderBox
153 164
   @override
154 165
   void attach(PipelineOwner owner) {
155 166
     super.attach(owner);
156
-    _showCursor.addListener(markNeedsPaint);
167
+    _showCursor.addListener(markNeedsCursorPaint);
157 168
     _renderContext.addBox(this);
158 169
   }
159 170
 
160 171
   @override
161 172
   void detach() {
162
-    _showCursor.removeListener(markNeedsPaint);
173
+    _showCursor.removeListener(markNeedsCursorPaint);
163 174
     _renderContext.removeBox(this);
164 175
     super.detach();
165 176
   }

+ 29
- 98
packages/zefyr/lib/src/widgets/editable_text.dart View File

@@ -1,9 +1,6 @@
1 1
 // Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2 2
 // for details. All rights reserved. Use of this source code is governed by a
3 3
 // BSD-style license that can be found in the LICENSE file.
4
-import 'dart:async';
5
-
6
-import 'package:collection/collection.dart';
7 4
 import 'package:flutter/cupertino.dart';
8 5
 import 'package:flutter/widgets.dart';
9 6
 import 'package:notus/notus.dart';
@@ -11,7 +8,7 @@ import 'package:notus/notus.dart';
11 8
 import 'code.dart';
12 9
 import 'common.dart';
13 10
 import 'controller.dart';
14
-import 'editable_box.dart';
11
+import 'cursor_timer.dart';
15 12
 import 'editor.dart';
16 13
 import 'image.dart';
17 14
 import 'input.dart';
@@ -19,11 +16,14 @@ import 'list.dart';
19 16
 import 'paragraph.dart';
20 17
 import 'quote.dart';
21 18
 import 'render_context.dart';
19
+import 'scope.dart';
22 20
 import 'selection.dart';
21
+import 'theme.dart';
23 22
 
24 23
 /// Core widget responsible for editing Zefyr documents.
25 24
 ///
26
-/// Depends on presence of [ZefyrTheme] somewhere up the widget tree.
25
+/// Depends on presence of [ZefyrTheme] and [ZefyrScope] somewhere up the
26
+/// widget tree.
27 27
 ///
28 28
 /// Consider using [ZefyrEditor] which wraps this widget and adds a toolbar to
29 29
 /// edit style attributes.
@@ -49,45 +49,10 @@ class ZefyrEditableText extends StatefulWidget {
49 49
   /// Padding around editable area.
50 50
   final EdgeInsets padding;
51 51
 
52
-  static ZefyrEditableTextScope of(BuildContext context) {
53
-    final ZefyrEditableTextScope result =
54
-        context.inheritFromWidgetOfExactType(ZefyrEditableTextScope);
55
-    return result;
56
-  }
57
-
58 52
   @override
59 53
   _ZefyrEditableTextState createState() => new _ZefyrEditableTextState();
60 54
 }
61 55
 
62
-/// Provides access to shared state of [ZefyrEditableText].
63
-class ZefyrEditableTextScope extends InheritedWidget {
64
-  static const _kEquality = const SetEquality<RenderEditableBox>();
65
-
66
-  ZefyrEditableTextScope({
67
-    Key key,
68
-    @required Widget child,
69
-    @required this.selection,
70
-    @required this.showCursor,
71
-    @required this.renderContext,
72
-    @required this.imageDelegate,
73
-  })  : _activeBoxes = new Set.from(renderContext.active),
74
-        super(key: key, child: child);
75
-
76
-  final TextSelection selection;
77
-  final ValueNotifier<bool> showCursor;
78
-  final ZefyrRenderContext renderContext;
79
-  final ZefyrImageDelegate imageDelegate;
80
-  final Set<RenderEditableBox> _activeBoxes;
81
-
82
-  @override
83
-  bool updateShouldNotify(ZefyrEditableTextScope oldWidget) {
84
-    return selection != oldWidget.selection ||
85
-        showCursor != oldWidget.showCursor ||
86
-        imageDelegate != oldWidget.imageDelegate ||
87
-        !_kEquality.equals(_activeBoxes, oldWidget._activeBoxes);
88
-  }
89
-}
90
-
91 56
 class _ZefyrEditableTextState extends State<ZefyrEditableText>
92 57
     with AutomaticKeepAliveClientMixin {
93 58
   //
@@ -102,8 +67,6 @@ class _ZefyrEditableTextState extends State<ZefyrEditableText>
102 67
 
103 68
   /// Current text selection.
104 69
   TextSelection get selection => widget.controller.selection;
105
-  ZefyrRenderContext get renderContext => _renderContext;
106
-  ValueNotifier<bool> get showCursor => _cursorTimer.value;
107 70
 
108 71
   /// Express interest in interacting with the keyboard.
109 72
   ///
@@ -127,7 +90,6 @@ class _ZefyrEditableTextState extends State<ZefyrEditableText>
127 90
   Widget build(BuildContext context) {
128 91
     FocusScope.of(context).reparentIfNeeded(focusNode);
129 92
     super.build(context); // See AutomaticKeepAliveState.
130
-    ZefyrEditor.of(context);
131 93
 
132 94
     Widget body = ListBody(children: _buildChildren(context));
133 95
     if (widget.padding != null) {
@@ -149,13 +111,7 @@ class _ZefyrEditableTextState extends State<ZefyrEditableText>
149 111
       ));
150 112
     }
151 113
 
152
-    return new ZefyrEditableTextScope(
153
-      selection: selection,
154
-      showCursor: showCursor,
155
-      renderContext: renderContext,
156
-      imageDelegate: widget.imageDelegate,
157
-      child: Stack(fit: StackFit.expand, children: layers),
158
-    );
114
+    return Stack(fit: StackFit.expand, children: layers);
159 115
   }
160 116
 
161 117
   @override
@@ -179,6 +135,22 @@ class _ZefyrEditableTextState extends State<ZefyrEditableText>
179 135
     }
180 136
   }
181 137
 
138
+  @override
139
+  void didChangeDependencies() {
140
+    super.didChangeDependencies();
141
+    final scope = ZefyrScope.of(context);
142
+    if (_renderContext != scope.renderContext) {
143
+      _renderContext?.removeListener(_handleRenderContextChange);
144
+      _renderContext = scope.renderContext;
145
+      _renderContext.addListener(_handleRenderContextChange);
146
+    }
147
+    if (_cursorTimer != scope.cursorTimer) {
148
+      _cursorTimer?.stop();
149
+      _cursorTimer = scope.cursorTimer;
150
+      _cursorTimer.startOrStop(focusNode, selection);
151
+    }
152
+  }
153
+
182 154
   @override
183 155
   void dispose() {
184 156
     _cancelSubscriptions();
@@ -196,9 +168,9 @@ class _ZefyrEditableTextState extends State<ZefyrEditableText>
196 168
   // Private members
197 169
   //
198 170
 
199
-  final ScrollController _scrollController = new ScrollController();
200
-  final ZefyrRenderContext _renderContext = new ZefyrRenderContext();
201
-  final _CursorTimer _cursorTimer = new _CursorTimer();
171
+  final ScrollController _scrollController = ScrollController();
172
+  ZefyrRenderContext _renderContext;
173
+  CursorTimer _cursorTimer;
202 174
   InputConnectionController _input;
203 175
   bool _didAutoFocus = false;
204 176
 
@@ -223,13 +195,13 @@ class _ZefyrEditableTextState extends State<ZefyrEditableText>
223 195
     final BlockNode block = node;
224 196
     final blockStyle = block.style.get(NotusAttribute.block);
225 197
     if (blockStyle == NotusAttribute.block.code) {
226
-      return new ZefyrCode(node: node);
198
+      return new ZefyrCode(node: block);
227 199
     } else if (blockStyle == NotusAttribute.block.bulletList) {
228
-      return new ZefyrList(node: node);
200
+      return new ZefyrList(node: block);
229 201
     } else if (blockStyle == NotusAttribute.block.numberList) {
230
-      return new ZefyrList(node: node);
202
+      return new ZefyrList(node: block);
231 203
     } else if (blockStyle == NotusAttribute.block.quote) {
232
-      return new ZefyrQuote(node: node);
204
+      return new ZefyrQuote(node: block);
233 205
     }
234 206
 
235 207
     throw new UnimplementedError('Block format $blockStyle.');
@@ -237,7 +209,6 @@ class _ZefyrEditableTextState extends State<ZefyrEditableText>
237 209
 
238 210
   void _updateSubscriptions([ZefyrEditableText oldWidget]) {
239 211
     if (oldWidget == null) {
240
-      _renderContext.addListener(_handleRenderContextChange);
241 212
       widget.controller.addListener(_handleLocalValueChange);
242 213
       focusNode.addListener(_handleFocusChange);
243 214
       return;
@@ -257,7 +228,6 @@ class _ZefyrEditableTextState extends State<ZefyrEditableText>
257 228
 
258 229
   void _cancelSubscriptions() {
259 230
     _renderContext.removeListener(_handleRenderContextChange);
260
-    _renderContext.dispose();
261 231
     widget.controller.removeListener(_handleLocalValueChange);
262 232
     focusNode.removeListener(_handleFocusChange);
263 233
     _input.closeConnection();
@@ -297,42 +267,3 @@ class _ZefyrEditableTextState extends State<ZefyrEditableText>
297 267
     });
298 268
   }
299 269
 }
300
-
301
-/// Helper class that keeps state relevant to the cursor.
302
-class _CursorTimer {
303
-  static const _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500);
304
-
305
-  Timer _timer;
306
-  final ValueNotifier<bool> _showCursor = new ValueNotifier<bool>(false);
307
-
308
-  ValueNotifier<bool> get value => _showCursor;
309
-
310
-  void _cursorTick(Timer timer) {
311
-    _showCursor.value = !_showCursor.value;
312
-  }
313
-
314
-  /// Starts cursor timer.
315
-  void start() {
316
-    _showCursor.value = true;
317
-    _timer = new Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick);
318
-  }
319
-
320
-  /// Stops cursor timer.
321
-  void stop() {
322
-    _timer?.cancel();
323
-    _timer = null;
324
-    _showCursor.value = false;
325
-  }
326
-
327
-  /// Starts or stops cursor timer based on current state of [focusNode]
328
-  /// and [selection].
329
-  void startOrStop(FocusNode focusNode, TextSelection selection) {
330
-    final hasFocus = focusNode.hasFocus;
331
-    final selectionCollapsed = selection.isCollapsed;
332
-    if (_timer == null && hasFocus && selectionCollapsed) {
333
-      start();
334
-    } else if (_timer != null && (!hasFocus || !selectionCollapsed)) {
335
-      stop();
336
-    }
337
-  }
338
-}

+ 13
- 167
packages/zefyr/lib/src/widgets/editor.dart View File

@@ -2,162 +2,15 @@
2 2
 // for details. All rights reserved. Use of this source code is governed by a
3 3
 // BSD-style license that can be found in the LICENSE file.
4 4
 import 'package:flutter/widgets.dart';
5
-import 'package:notus/notus.dart';
6 5
 
7 6
 import 'controller.dart';
8 7
 import 'editable_text.dart';
9 8
 import 'image.dart';
10 9
 import 'scaffold.dart';
10
+import 'scope.dart';
11 11
 import 'theme.dart';
12 12
 import 'toolbar.dart';
13 13
 
14
-class ZefyrEditorScope extends ChangeNotifier {
15
-  ZefyrEditorScope({
16
-    @required ZefyrImageDelegate imageDelegate,
17
-    @required ZefyrController controller,
18
-    @required FocusNode focusNode,
19
-    @required FocusScopeNode focusScope,
20
-  })  : _controller = controller,
21
-        _imageDelegate = imageDelegate,
22
-        _focusScope = focusScope,
23
-        _focusNode = focusNode {
24
-    _selectionStyle = _controller.getSelectionStyle();
25
-    _selection = _controller.selection;
26
-    _controller.addListener(_handleControllerChange);
27
-    _focusNode.addListener(_handleFocusChange);
28
-  }
29
-
30
-  bool _disposed = false;
31
-
32
-  ZefyrImageDelegate _imageDelegate;
33
-  ZefyrImageDelegate get imageDelegate => _imageDelegate;
34
-
35
-  FocusScopeNode _focusScope;
36
-  FocusNode _focusNode;
37
-
38
-  ZefyrController _controller;
39
-  NotusStyle get selectionStyle => _selectionStyle;
40
-  NotusStyle _selectionStyle;
41
-  TextSelection get selection => _selection;
42
-  TextSelection _selection;
43
-
44
-  @override
45
-  void dispose() {
46
-    assert(!_disposed);
47
-    _controller.removeListener(_handleControllerChange);
48
-    _focusNode.removeListener(_handleFocusChange);
49
-    _disposed = true;
50
-    super.dispose();
51
-  }
52
-
53
-  void _updateControllerIfNeeded(ZefyrController value) {
54
-    if (_controller != value) {
55
-      _controller.removeListener(_handleControllerChange);
56
-      _controller = value;
57
-      _selectionStyle = _controller.getSelectionStyle();
58
-      _selection = _controller.selection;
59
-      _controller.addListener(_handleControllerChange);
60
-      notifyListeners();
61
-    }
62
-  }
63
-
64
-  void _updateFocusNodeIfNeeded(FocusNode value) {
65
-    if (_focusNode != value) {
66
-      _focusNode.removeListener(_handleFocusChange);
67
-      _focusNode = value;
68
-      _focusNode.addListener(_handleFocusChange);
69
-      notifyListeners();
70
-    }
71
-  }
72
-
73
-  void _updateImageDelegateIfNeeded(ZefyrImageDelegate value) {
74
-    if (_imageDelegate != value) {
75
-      _imageDelegate = value;
76
-      notifyListeners();
77
-    }
78
-  }
79
-
80
-  void _handleControllerChange() {
81
-    assert(!_disposed);
82
-    final attrs = _controller.getSelectionStyle();
83
-    final selection = _controller.selection;
84
-    if (_selectionStyle != attrs || _selection != selection) {
85
-      _selectionStyle = attrs;
86
-      _selection = _controller.selection;
87
-      notifyListeners();
88
-    }
89
-  }
90
-
91
-  void _handleFocusChange() {
92
-    assert(!_disposed);
93
-    if (focusOwner == FocusOwner.none && !_selection.isCollapsed) {
94
-      // Collapse selection if there is nothing focused.
95
-      _controller.updateSelection(_selection.copyWith(
96
-        baseOffset: _selection.extentOffset,
97
-        extentOffset: _selection.extentOffset,
98
-      ));
99
-    }
100
-    notifyListeners();
101
-  }
102
-
103
-  FocusNode _toolbarFocusNode;
104
-
105
-  void setToolbarFocusNode(FocusNode node) {
106
-    assert(!_disposed || node == null);
107
-    if (_toolbarFocusNode != node) {
108
-      _toolbarFocusNode?.removeListener(_handleFocusChange);
109
-      _toolbarFocusNode = node;
110
-      _toolbarFocusNode?.addListener(_handleFocusChange);
111
-      // We do not notify listeners here because it will happen when
112
-      // focus state changes, see [_handleFocusChange].
113
-    }
114
-  }
115
-
116
-  FocusOwner get focusOwner {
117
-    assert(!_disposed);
118
-    if (_focusNode.hasFocus) {
119
-      return FocusOwner.editor;
120
-    } else if (_toolbarFocusNode?.hasFocus == true) {
121
-      return FocusOwner.toolbar;
122
-    } else {
123
-      return FocusOwner.none;
124
-    }
125
-  }
126
-
127
-  void updateSelection(TextSelection value,
128
-      {ChangeSource source: ChangeSource.remote}) {
129
-    assert(!_disposed);
130
-    _controller.updateSelection(value, source: source);
131
-  }
132
-
133
-  void formatSelection(NotusAttribute value) {
134
-    assert(!_disposed);
135
-    _controller.formatSelection(value);
136
-  }
137
-
138
-  void focus() {
139
-    assert(!_disposed);
140
-    _focusScope.requestFocus(_focusNode);
141
-  }
142
-
143
-  void hideKeyboard() {
144
-    assert(!_disposed);
145
-    _focusNode.unfocus();
146
-  }
147
-}
148
-
149
-class _ZefyrEditorScope extends InheritedWidget {
150
-  final ZefyrEditorScope scope;
151
-
152
-  _ZefyrEditorScope({Key key, Widget child, @required this.scope})
153
-      : super(key: key, child: child);
154
-
155
-  @override
156
-  bool updateShouldNotify(_ZefyrEditorScope oldWidget) {
157
-    return oldWidget.scope != scope;
158
-  }
159
-}
160
-
161 14
 /// Widget for editing Zefyr documents.
162 15
 class ZefyrEditor extends StatefulWidget {
163 16
   const ZefyrEditor({
@@ -183,19 +36,13 @@ class ZefyrEditor extends StatefulWidget {
183 36
   /// Padding around editable area.
184 37
   final EdgeInsets padding;
185 38
 
186
-  static ZefyrEditorScope of(BuildContext context) {
187
-    _ZefyrEditorScope widget =
188
-        context.inheritFromWidgetOfExactType(_ZefyrEditorScope);
189
-    return widget.scope;
190
-  }
191
-
192 39
   @override
193 40
   _ZefyrEditorState createState() => new _ZefyrEditorState();
194 41
 }
195 42
 
196 43
 class _ZefyrEditorState extends State<ZefyrEditor> {
197 44
   ZefyrImageDelegate _imageDelegate;
198
-  ZefyrEditorScope _scope;
45
+  ZefyrScope _scope;
199 46
   ZefyrThemeData _themeData;
200 47
   GlobalKey<ZefyrToolbarState> _toolbarKey;
201 48
   ZefyrScaffoldState _scaffold;
@@ -214,7 +61,7 @@ class _ZefyrEditorState extends State<ZefyrEditor> {
214 61
     _toolbarKey = null;
215 62
   }
216 63
 
217
-  Widget buildToolbar(BuildContext) {
64
+  Widget buildToolbar(BuildContext context) {
218 65
     return ZefyrTheme(
219 66
       data: _themeData,
220 67
       child: ZefyrToolbar(
@@ -231,6 +78,7 @@ class _ZefyrEditorState extends State<ZefyrEditor> {
231 78
     } else if (!hasToolbar) {
232 79
       showToolbar();
233 80
     } else {
81
+      // TODO: is there a nicer way to do this?
234 82
       WidgetsBinding.instance.addPostFrameCallback((_) {
235 83
         _toolbarKey?.currentState?.markNeedsRebuild();
236 84
       });
@@ -246,11 +94,11 @@ class _ZefyrEditorState extends State<ZefyrEditor> {
246 94
   @override
247 95
   void didUpdateWidget(ZefyrEditor oldWidget) {
248 96
     super.didUpdateWidget(oldWidget);
249
-    _scope._updateControllerIfNeeded(widget.controller);
250
-    _scope._updateFocusNodeIfNeeded(widget.focusNode);
97
+    _scope.controller = widget.controller;
98
+    _scope.focusNode = widget.focusNode;
251 99
     if (widget.imageDelegate != oldWidget.imageDelegate) {
252 100
       _imageDelegate = widget.imageDelegate ?? new ZefyrDefaultImageDelegate();
253
-      _scope._updateImageDelegateIfNeeded(_imageDelegate);
101
+      _scope.imageDelegate = _imageDelegate;
254 102
     }
255 103
   }
256 104
 
@@ -264,7 +112,7 @@ class _ZefyrEditorState extends State<ZefyrEditor> {
264 112
         : fallbackTheme;
265 113
 
266 114
     if (_scope == null) {
267
-      _scope = ZefyrEditorScope(
115
+      _scope = ZefyrScope.editable(
268 116
         imageDelegate: _imageDelegate,
269 117
         controller: widget.controller,
270 118
         focusNode: widget.focusNode,
@@ -273,9 +121,7 @@ class _ZefyrEditorState extends State<ZefyrEditor> {
273 121
       _scope.addListener(_handleChange);
274 122
     } else {
275 123
       final focusScope = FocusScope.of(context);
276
-      if (focusScope != _scope._focusScope) {
277
-        _scope._focusScope = focusScope;
278
-      }
124
+      _scope.focusScope = focusScope;
279 125
     }
280 126
 
281 127
     final scaffold = ZefyrScaffold.of(context);
@@ -298,9 +144,9 @@ class _ZefyrEditorState extends State<ZefyrEditor> {
298 144
   @override
299 145
   Widget build(BuildContext context) {
300 146
     Widget editable = new ZefyrEditableText(
301
-      controller: widget.controller,
302
-      focusNode: widget.focusNode,
303
-      imageDelegate: _imageDelegate,
147
+      controller: _scope.controller,
148
+      focusNode: _scope.focusNode,
149
+      imageDelegate: _scope.imageDelegate,
304 150
       autofocus: widget.autofocus,
305 151
       enabled: widget.enabled,
306 152
       padding: widget.padding,
@@ -309,7 +155,7 @@ class _ZefyrEditorState extends State<ZefyrEditor> {
309 155
 
310 156
     return ZefyrTheme(
311 157
       data: _themeData,
312
-      child: _ZefyrEditorScope(
158
+      child: ZefyrScopeAccess(
313 159
         scope: _scope,
314 160
         child: editable,
315 161
       ),

+ 1
- 1
packages/zefyr/lib/src/widgets/image.dart View File

@@ -54,7 +54,7 @@ class ZefyrImage extends StatefulWidget {
54 54
 class _ZefyrImageState extends State<ZefyrImage> {
55 55
   String get imageSource {
56 56
     EmbedAttribute attribute = widget.node.style.get(NotusAttribute.embed);
57
-    return attribute.value['source'];
57
+    return attribute.value['source'] as String;
58 58
   }
59 59
 
60 60
   @override

+ 2
- 2
packages/zefyr/lib/src/widgets/quote.dart View File

@@ -36,9 +36,9 @@ class ZefyrQuote extends StatelessWidget {
36 36
 
37 37
     Widget content;
38 38
     if (line.style.contains(NotusAttribute.heading)) {
39
-      content = new ZefyrHeading(node: node, blockStyle: blockStyle);
39
+      content = new ZefyrHeading(node: line, blockStyle: blockStyle);
40 40
     } else {
41
-      content = new ZefyrParagraph(node: node, blockStyle: blockStyle);
41
+      content = new ZefyrParagraph(node: line, blockStyle: blockStyle);
42 42
     }
43 43
 
44 44
     final row = Row(children: <Widget>[Expanded(child: content)]);

+ 3
- 2
packages/zefyr/lib/src/widgets/render_context.dart View File

@@ -97,9 +97,10 @@ class ZefyrRenderContext extends ChangeNotifier {
97 97
   /// Returns closest render box to the specified global [point].
98 98
   ///
99 99
   /// If [point] is inside of one of active render boxes that box is returned.
100
-  /// Otherwise this method checks if [point] is to the left or to the right
100
+  /// If no box found this method checks if [point] is to the left or to the right
101 101
   /// side of a box, e.g. if vertical offset of this point is inside of one of
102
-  /// the active boxes. If it is then that box is returned.
102
+  /// the active boxes. If it is then that box is returned. If not then this
103
+  /// method picks a box with shortest vertical distance to this [point].
103 104
   RenderEditableProxyBox closestBoxForGlobalPoint(Offset point) {
104 105
     assert(!_disposed);
105 106
     RenderEditableProxyBox box = boxForGlobalPoint(point);

+ 8
- 7
packages/zefyr/lib/src/widgets/rich_text.dart View File

@@ -11,8 +11,9 @@ import 'package:notus/notus.dart';
11 11
 import 'caret.dart';
12 12
 import 'editable_box.dart';
13 13
 
14
-class EditableRichText extends LeafRenderObjectWidget {
15
-  EditableRichText({
14
+/// Represents single paragraph of Zefyr rich-text.
15
+class ZefyrRichText extends LeafRenderObjectWidget {
16
+  ZefyrRichText({
16 17
     @required this.node,
17 18
     @required this.text,
18 19
   }) : assert(node != null && text != null);
@@ -22,7 +23,7 @@ class EditableRichText extends LeafRenderObjectWidget {
22 23
 
23 24
   @override
24 25
   RenderObject createRenderObject(BuildContext context) {
25
-    return new RenderEditableParagraph(
26
+    return new RenderZefyrParagraph(
26 27
       text,
27 28
       node: node,
28 29
       textDirection: Directionality.of(context),
@@ -31,18 +32,18 @@ class EditableRichText extends LeafRenderObjectWidget {
31 32
 
32 33
   @override
33 34
   void updateRenderObject(
34
-      BuildContext context, RenderEditableParagraph renderObject) {
35
+      BuildContext context, RenderZefyrParagraph renderObject) {
35 36
     renderObject
36 37
       ..text = text
37 38
       ..node = node;
38 39
   }
39 40
 }
40 41
 
41
-class RenderEditableParagraph extends RenderParagraph
42
+class RenderZefyrParagraph extends RenderParagraph
42 43
     implements RenderEditableBox {
43
-  RenderEditableParagraph(
44
+  RenderZefyrParagraph(
44 45
     TextSpan text, {
45
-    @required ContainerNode node,
46
+    @required LineNode node,
46 47
     TextAlign textAlign: TextAlign.start,
47 48
     @required TextDirection textDirection,
48 49
     bool softWrap: true,

+ 232
- 0
packages/zefyr/lib/src/widgets/scope.dart View File

@@ -0,0 +1,232 @@
1
+import 'package:flutter/foundation.dart';
2
+import 'package:flutter/material.dart';
3
+import 'package:notus/notus.dart';
4
+
5
+import 'controller.dart';
6
+import 'cursor_timer.dart';
7
+import 'editor.dart';
8
+import 'image.dart';
9
+import 'render_context.dart';
10
+import 'view.dart';
11
+
12
+/// Provides access to shared state of [ZefyrEditor] or [ZefyrView].
13
+///
14
+/// A scope object can be created by an editable widget like [ZefyrEditor] in
15
+/// which case it provides access to editing state, including focus nodes,
16
+/// selection and such. Editable scope can be created using
17
+/// [ZefyrScope.editable] constructor.
18
+///
19
+/// If a scope object is created by a view-only widget like [ZefyrView] then
20
+/// it only provides access to [imageDelegate].
21
+///
22
+/// Can be retrieved using [ZefyrScope.of].
23
+class ZefyrScope extends ChangeNotifier {
24
+  /// Creates a view-only scope.
25
+  ///
26
+  /// Normally used in [ZefyrView].
27
+  ZefyrScope.view({@required ZefyrImageDelegate imageDelegate})
28
+      : assert(imageDelegate != null),
29
+        isEditable = false,
30
+        _imageDelegate = imageDelegate;
31
+
32
+  /// Creates editable scope.
33
+  ///
34
+  /// Normally used in [ZefyrEditor].
35
+  ZefyrScope.editable({
36
+    @required ZefyrController controller,
37
+    @required ZefyrImageDelegate imageDelegate,
38
+    @required FocusNode focusNode,
39
+    @required FocusScopeNode focusScope,
40
+  })  : assert(controller != null),
41
+        assert(imageDelegate != null),
42
+        assert(focusNode != null),
43
+        assert(focusScope != null),
44
+        isEditable = true,
45
+        _controller = controller,
46
+        _imageDelegate = imageDelegate,
47
+        _focusNode = focusNode,
48
+        _focusScope = focusScope,
49
+        _cursorTimer = CursorTimer(),
50
+        _renderContext = ZefyrRenderContext() {
51
+    _selectionStyle = _controller.getSelectionStyle();
52
+    _selection = _controller.selection;
53
+    _controller.addListener(_handleControllerChange);
54
+    _focusNode.addListener(_handleFocusChange);
55
+  }
56
+
57
+  static ZefyrScope of(BuildContext context) {
58
+    final ZefyrScopeAccess widget =
59
+        context.inheritFromWidgetOfExactType(ZefyrScopeAccess);
60
+    return widget.scope;
61
+  }
62
+
63
+  ZefyrImageDelegate _imageDelegate;
64
+  ZefyrImageDelegate get imageDelegate => _imageDelegate;
65
+  set imageDelegate(ZefyrImageDelegate value) {
66
+    assert(value != null);
67
+    if (_imageDelegate != value) {
68
+      _imageDelegate = value;
69
+      notifyListeners();
70
+    }
71
+  }
72
+
73
+  ZefyrController _controller;
74
+  ZefyrController get controller => _controller;
75
+  set controller(ZefyrController value) {
76
+    assert(isEditable && value != null);
77
+    if (_controller != value) {
78
+      _controller.removeListener(_handleControllerChange);
79
+      _controller = value;
80
+      _selectionStyle = _controller.getSelectionStyle();
81
+      _selection = _controller.selection;
82
+      _controller.addListener(_handleControllerChange);
83
+      notifyListeners();
84
+    }
85
+  }
86
+
87
+  FocusNode _focusNode;
88
+  FocusNode get focusNode => _focusNode;
89
+  set focusNode(FocusNode value) {
90
+    assert(isEditable && value != null);
91
+    if (_focusNode != value) {
92
+      _focusNode.removeListener(_handleFocusChange);
93
+      _focusNode = value;
94
+      _focusNode.addListener(_handleFocusChange);
95
+      notifyListeners();
96
+    }
97
+  }
98
+
99
+  FocusScopeNode _focusScope;
100
+  FocusScopeNode get focusScope => _focusScope;
101
+  set focusScope(FocusScopeNode value) {
102
+    assert(isEditable && value != null);
103
+    if (_focusScope != value) {
104
+      _focusScope = value;
105
+    }
106
+  }
107
+
108
+  CursorTimer _cursorTimer;
109
+  CursorTimer get cursorTimer => _cursorTimer;
110
+  ValueNotifier<bool> get showCursor => cursorTimer.value;
111
+
112
+  ZefyrRenderContext _renderContext;
113
+  ZefyrRenderContext get renderContext => _renderContext;
114
+
115
+  NotusStyle get selectionStyle => _selectionStyle;
116
+  NotusStyle _selectionStyle;
117
+  TextSelection get selection => _selection;
118
+  TextSelection _selection;
119
+
120
+  bool _disposed = false;
121
+  FocusNode _toolbarFocusNode;
122
+
123
+  /// Whether this scope is backed by editable Zefyr widgets or read-only view.
124
+  ///
125
+  /// Returns `true` if this scope provides Zefyr interface that allows editing
126
+  /// (e.g. created by [ZefyrEditor]). Returns `false` if this scope provides
127
+  /// read-only view (e.g. created by [ZefyrView]).
128
+  ///
129
+  /// Editable scope provides access to corresponding [controller], [focusNode],
130
+  /// [focusScope], [showCursor], [renderContext] and other shared objects. For
131
+  /// non-editable scopes these are set to `null`. You can still access
132
+  /// objects which are not dependent on editing flow, e.g. [imageDelegate].
133
+  final bool isEditable;
134
+
135
+  set toolbarFocusNode(FocusNode node) {
136
+    assert(isEditable);
137
+    assert(!_disposed || node == null);
138
+    if (_toolbarFocusNode != node) {
139
+      _toolbarFocusNode?.removeListener(_handleFocusChange);
140
+      _toolbarFocusNode = node;
141
+      _toolbarFocusNode?.addListener(_handleFocusChange);
142
+      // We do not notify listeners here because it will happen when
143
+      // focus state changes, see [_handleFocusChange].
144
+    }
145
+  }
146
+
147
+  FocusOwner get focusOwner {
148
+    assert(isEditable);
149
+    assert(!_disposed);
150
+    if (_focusNode.hasFocus) {
151
+      return FocusOwner.editor;
152
+    } else if (_toolbarFocusNode?.hasFocus == true) {
153
+      return FocusOwner.toolbar;
154
+    } else {
155
+      return FocusOwner.none;
156
+    }
157
+  }
158
+
159
+  void updateSelection(TextSelection value,
160
+      {ChangeSource source: ChangeSource.remote}) {
161
+    assert(isEditable);
162
+    assert(!_disposed);
163
+    _controller.updateSelection(value, source: source);
164
+  }
165
+
166
+  void formatSelection(NotusAttribute value) {
167
+    assert(isEditable);
168
+    assert(!_disposed);
169
+    _controller.formatSelection(value);
170
+  }
171
+
172
+  void focus() {
173
+    assert(isEditable);
174
+    assert(!_disposed);
175
+    _focusScope.requestFocus(_focusNode);
176
+  }
177
+
178
+  void hideKeyboard() {
179
+    assert(isEditable);
180
+    assert(!_disposed);
181
+    _focusNode.unfocus();
182
+  }
183
+
184
+  @override
185
+  void dispose() {
186
+    assert(!_disposed);
187
+    _controller?.removeListener(_handleControllerChange);
188
+    _focusNode?.removeListener(_handleFocusChange);
189
+    _disposed = true;
190
+    super.dispose();
191
+  }
192
+
193
+  void _handleControllerChange() {
194
+    assert(!_disposed);
195
+    final attrs = _controller.getSelectionStyle();
196
+    final selection = _controller.selection;
197
+    if (_selectionStyle != attrs || _selection != selection) {
198
+      _selectionStyle = attrs;
199
+      _selection = selection;
200
+      notifyListeners();
201
+    }
202
+  }
203
+
204
+  void _handleFocusChange() {
205
+    assert(!_disposed);
206
+    if (focusOwner == FocusOwner.none && !_selection.isCollapsed) {
207
+      // Collapse selection if there is nothing focused.
208
+      _controller.updateSelection(_selection.copyWith(
209
+        baseOffset: _selection.extentOffset,
210
+        extentOffset: _selection.extentOffset,
211
+      ));
212
+    }
213
+    notifyListeners();
214
+  }
215
+
216
+  @override
217
+  String toString() {
218
+    return '$ZefyrScope#${shortHash(this)}';
219
+  }
220
+}
221
+
222
+class ZefyrScopeAccess extends InheritedWidget {
223
+  final ZefyrScope scope;
224
+
225
+  ZefyrScopeAccess({Key key, @required this.scope, @required Widget child})
226
+      : super(key: key, child: child);
227
+
228
+  @override
229
+  bool updateShouldNotify(ZefyrScopeAccess oldWidget) {
230
+    return scope != oldWidget.scope;
231
+  }
232
+}

+ 41
- 28
packages/zefyr/lib/src/widgets/selection.dart View File

@@ -10,13 +10,12 @@ import 'package:zefyr/util.dart';
10 10
 
11 11
 import 'controller.dart';
12 12
 import 'editable_box.dart';
13
-import 'editable_text.dart';
14
-import 'editor.dart';
13
+import 'scope.dart';
15 14
 
16 15
 RenderEditableBox _getEditableBox(HitTestResult result) {
17 16
   for (var entry in result.path) {
18 17
     if (entry.target is RenderEditableBox) {
19
-      return entry.target;
18
+      return entry.target as RenderEditableBox;
20 19
     }
21 20
   }
22 21
   return null;
@@ -73,14 +72,14 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
73 72
   }
74 73
 
75 74
   void showToolbar() {
76
-    final editable = ZefyrEditableText.of(context);
77
-    assert(editable != null);
75
+    final scope = ZefyrScope.of(context);
76
+    assert(scope != null);
78 77
     final toolbarOpacity = _toolbarController.view;
79 78
     _toolbar = new OverlayEntry(
80 79
       builder: (context) => new FadeTransition(
81 80
             opacity: toolbarOpacity,
82 81
             child: new _SelectionToolbar(
83
-              editable: editable,
82
+              scope: scope,
84 83
               controls: widget.controls,
85 84
               delegate: this,
86 85
             ),
@@ -117,7 +116,7 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
117 116
   @override
118 117
   void didChangeDependencies() {
119 118
     super.didChangeDependencies();
120
-    final editor = ZefyrEditor.of(context);
119
+    final editor = ZefyrScope.of(context);
121 120
     if (_editor != editor) {
122 121
       _editor?.removeListener(_handleChange);
123 122
       _editor = editor;
@@ -174,7 +173,7 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
174 173
   OverlayEntry _toolbar;
175 174
   AnimationController _toolbarController;
176 175
 
177
-  ZefyrEditorScope _editor;
176
+  ZefyrScope _editor;
178 177
   TextSelection _selection;
179 178
   FocusOwner _focusOwner;
180 179
 
@@ -190,9 +189,9 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
190 189
     if (!mounted) {
191 190
       return;
192 191
     }
193
-    final editor = ZefyrEditor.of(context);
194
-    final selection = editor.selection;
195
-    final focusOwner = editor.focusOwner;
192
+
193
+    final selection = _editor.selection;
194
+    final focusOwner = _editor.focusOwner;
196 195
     setState(() {
197 196
       if (focusOwner != FocusOwner.editor) {
198 197
         hideToolbar();
@@ -233,8 +232,7 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
233 232
 
234 233
     RenderEditableProxyBox box = _getEditableBox(result);
235 234
     if (box == null) {
236
-      final editable = ZefyrEditableText.of(context);
237
-      box = editable.renderContext.closestBoxForGlobalPoint(globalPoint);
235
+      box = _editor.renderContext.closestBoxForGlobalPoint(globalPoint);
238 236
     }
239 237
     if (box == null) return null;
240 238
 
@@ -295,6 +293,8 @@ class SelectionHandleDriver extends StatefulWidget {
295 293
 }
296 294
 
297 295
 class _SelectionHandleDriverState extends State<SelectionHandleDriver> {
296
+  ZefyrScope _scope;
297
+
298 298
   /// Current document selection.
299 299
   TextSelection get selection => _selection;
300 300
   TextSelection _selection;
@@ -327,8 +327,19 @@ class _SelectionHandleDriverState extends State<SelectionHandleDriver> {
327 327
   @override
328 328
   void didChangeDependencies() {
329 329
     super.didChangeDependencies();
330
-    final editable = ZefyrEditableText.of(context);
331
-    _selection = editable.selection;
330
+    final scope = ZefyrScope.of(context);
331
+    if (_scope != scope) {
332
+      _scope?.removeListener(_handleScopeChange);
333
+      _scope = scope;
334
+      _scope.addListener(_handleScopeChange);
335
+    }
336
+    _selection = _scope.selection;
337
+  }
338
+
339
+  @override
340
+  void dispose() {
341
+    _scope?.removeListener(_handleScopeChange);
342
+    super.dispose();
332 343
   }
333 344
 
334 345
   //
@@ -337,15 +348,13 @@ class _SelectionHandleDriverState extends State<SelectionHandleDriver> {
337 348
 
338 349
   @override
339 350
   Widget build(BuildContext context) {
340
-    final editor = ZefyrEditor.of(context);
341
-    final editable = ZefyrEditableText.of(context);
342 351
     if (selection == null ||
343 352
         selection.isCollapsed ||
344 353
         widget.controls == null ||
345
-        editor.focusOwner != FocusOwner.editor) {
354
+        _scope.focusOwner != FocusOwner.editor) {
346 355
       return new Container();
347 356
     }
348
-    final block = editable.renderContext.boxForTextOffset(documentOffset);
357
+    final block = _scope.renderContext.boxForTextOffset(documentOffset);
349 358
     final position = getPosition(block);
350 359
     Widget handle;
351 360
     if (position == null) {
@@ -388,6 +397,14 @@ class _SelectionHandleDriverState extends State<SelectionHandleDriver> {
388 397
 
389 398
   Offset _dragPosition;
390 399
 
400
+  void _handleScopeChange() {
401
+    if (_selection != _scope.selection) {
402
+      setState(() {
403
+        _selection = _scope.selection;
404
+      });
405
+    }
406
+  }
407
+
391 408
   void _handleDragStart(DragStartDetails details) {
392 409
     _dragPosition = details.globalPosition;
393 410
   }
@@ -395,9 +412,7 @@ class _SelectionHandleDriverState extends State<SelectionHandleDriver> {
395 412
   void _handleDragUpdate(DragUpdateDetails details) {
396 413
     _dragPosition += details.delta;
397 414
     final globalPoint = _dragPosition;
398
-    final editor = ZefyrEditor.of(context);
399
-    final editable = ZefyrEditableText.of(context);
400
-    final paragraph = editable.renderContext.boxForGlobalPoint(globalPoint);
415
+    final paragraph = _scope.renderContext.boxForGlobalPoint(globalPoint);
401 416
     if (paragraph == null) {
402 417
       return;
403 418
     }
@@ -414,7 +429,7 @@ class _SelectionHandleDriverState extends State<SelectionHandleDriver> {
414 429
     }
415 430
 
416 431
     if (newSelection != _selection) {
417
-      editor.updateSelection(newSelection, source: ChangeSource.local);
432
+      _scope.updateSelection(newSelection, source: ChangeSource.local);
418 433
     }
419 434
   }
420 435
 }
@@ -422,12 +437,12 @@ class _SelectionHandleDriverState extends State<SelectionHandleDriver> {
422 437
 class _SelectionToolbar extends StatefulWidget {
423 438
   const _SelectionToolbar({
424 439
     Key key,
425
-    @required this.editable,
440
+    @required this.scope,
426 441
     @required this.controls,
427 442
     @required this.delegate,
428 443
   }) : super(key: key);
429 444
 
430
-  final ZefyrEditableTextScope editable;
445
+  final ZefyrScope scope;
431 446
   final TextSelectionControls controls;
432 447
   final TextSelectionDelegate delegate;
433 448
 
@@ -436,7 +451,7 @@ class _SelectionToolbar extends StatefulWidget {
436 451
 }
437 452
 
438 453
 class _SelectionToolbarState extends State<_SelectionToolbar> {
439
-  ZefyrEditableTextScope get editable => widget.editable;
454
+  ZefyrScope get editable => widget.scope;
440 455
   TextSelection get selection => widget.delegate.textEditingValue.selection;
441 456
 
442 457
   @override
@@ -452,7 +467,6 @@ class _SelectionToolbarState extends State<_SelectionToolbar> {
452 467
       return Container();
453 468
     }
454 469
     final boxes = block.getEndpointsForSelection(selection);
455
-
456 470
     // Find the horizontal midpoint, just above the selected text.
457 471
     final Offset midpoint = new Offset(
458 472
       (boxes.length == 1)
@@ -465,7 +479,6 @@ class _SelectionToolbarState extends State<_SelectionToolbar> {
465 479
       block.localToGlobal(Offset.zero),
466 480
       block.localToGlobal(block.size.bottomRight(Offset.zero)),
467 481
     );
468
-
469 482
     final toolbar = widget.controls
470 483
         .buildToolbar(context, editingRegion, midpoint, widget.delegate);
471 484
     return new CompositedTransformFollower(

+ 3
- 3
packages/zefyr/lib/src/widgets/toolbar.dart View File

@@ -8,7 +8,7 @@ import 'package:flutter/material.dart';
8 8
 import 'package:notus/notus.dart';
9 9
 
10 10
 import 'buttons.dart';
11
-import 'editor.dart';
11
+import 'scope.dart';
12 12
 import 'theme.dart';
13 13
 
14 14
 /// List of all button actions supported by [ZefyrToolbar] buttons.
@@ -107,7 +107,7 @@ class ZefyrToolbar extends StatefulWidget implements PreferredSizeWidget {
107 107
   }) : super(key: key);
108 108
 
109 109
   final ZefyrToolbarDelegate delegate;
110
-  final ZefyrEditorScope editor;
110
+  final ZefyrScope editor;
111 111
 
112 112
   /// Whether to automatically hide this toolbar when editor loses focus.
113 113
   final bool autoHide;
@@ -187,7 +187,7 @@ class ZefyrToolbarState extends State<ZefyrToolbar>
187 187
 
188 188
   bool get hasOverlay => _overlayBuilder != null;
189 189
 
190
-  ZefyrEditorScope get editor => widget.editor;
190
+  ZefyrScope get editor => widget.editor;
191 191
 
192 192
   @override
193 193
   void initState() {

+ 105
- 0
packages/zefyr/lib/src/widgets/view.dart View File

@@ -0,0 +1,105 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:notus/notus.dart';
3
+
4
+import 'code.dart';
5
+import 'common.dart';
6
+import 'image.dart';
7
+import 'list.dart';
8
+import 'paragraph.dart';
9
+import 'quote.dart';
10
+import 'scope.dart';
11
+import 'theme.dart';
12
+
13
+/// Non-scrollable read-only view of Notus rich text documents.
14
+class ZefyrView extends StatefulWidget {
15
+  final NotusDocument document;
16
+  final ZefyrImageDelegate imageDelegate;
17
+
18
+  const ZefyrView({Key key, @required this.document, this.imageDelegate})
19
+      : super(key: key);
20
+
21
+  @override
22
+  ZefyrViewState createState() => ZefyrViewState();
23
+}
24
+
25
+class ZefyrViewState extends State<ZefyrView> {
26
+  ZefyrScope _scope;
27
+  ZefyrThemeData _themeData;
28
+
29
+  ZefyrImageDelegate get imageDelegate => widget.imageDelegate;
30
+
31
+  @override
32
+  void initState() {
33
+    super.initState();
34
+    _scope = ZefyrScope.view(imageDelegate: widget.imageDelegate);
35
+  }
36
+
37
+  @override
38
+  void didUpdateWidget(ZefyrView oldWidget) {
39
+    super.didUpdateWidget(oldWidget);
40
+    _scope.imageDelegate = widget.imageDelegate;
41
+  }
42
+
43
+  @override
44
+  void didChangeDependencies() {
45
+    super.didChangeDependencies();
46
+    final parentTheme = ZefyrTheme.of(context, nullOk: true);
47
+    final fallbackTheme = ZefyrThemeData.fallback(context);
48
+    _themeData = (parentTheme != null)
49
+        ? fallbackTheme.merge(parentTheme)
50
+        : fallbackTheme;
51
+  }
52
+
53
+  @override
54
+  void dispose() {
55
+    _scope.dispose();
56
+    super.dispose();
57
+  }
58
+
59
+  @override
60
+  Widget build(BuildContext context) {
61
+    return ZefyrTheme(
62
+      data: _themeData,
63
+      child: ZefyrScopeAccess(
64
+        scope: _scope,
65
+        child: Column(
66
+          crossAxisAlignment: CrossAxisAlignment.stretch,
67
+          children: _buildChildren(context),
68
+        ),
69
+      ),
70
+    );
71
+  }
72
+
73
+  List<Widget> _buildChildren(BuildContext context) {
74
+    final result = <Widget>[];
75
+    for (var node in widget.document.root.children) {
76
+      result.add(_defaultChildBuilder(context, node));
77
+    }
78
+    return result;
79
+  }
80
+
81
+  Widget _defaultChildBuilder(BuildContext context, Node node) {
82
+    if (node is LineNode) {
83
+      if (node.hasEmbed) {
84
+        return new RawZefyrLine(node: node);
85
+      } else if (node.style.contains(NotusAttribute.heading)) {
86
+        return new ZefyrHeading(node: node);
87
+      }
88
+      return new ZefyrParagraph(node: node);
89
+    }
90
+
91
+    final BlockNode block = node;
92
+    final blockStyle = block.style.get(NotusAttribute.block);
93
+    if (blockStyle == NotusAttribute.block.code) {
94
+      return new ZefyrCode(node: block);
95
+    } else if (blockStyle == NotusAttribute.block.bulletList) {
96
+      return new ZefyrList(node: block);
97
+    } else if (blockStyle == NotusAttribute.block.numberList) {
98
+      return new ZefyrList(node: block);
99
+    } else if (blockStyle == NotusAttribute.block.quote) {
100
+      return new ZefyrQuote(node: block);
101
+    }
102
+
103
+    throw new UnimplementedError('Block format $blockStyle.');
104
+  }
105
+}

+ 2
- 0
packages/zefyr/lib/zefyr.dart View File

@@ -22,6 +22,8 @@ export 'src/widgets/list.dart';
22 22
 export 'src/widgets/paragraph.dart';
23 23
 export 'src/widgets/quote.dart';
24 24
 export 'src/widgets/scaffold.dart';
25
+export 'src/widgets/scope.dart' hide ZefyrScopeAccess;
25 26
 export 'src/widgets/selection.dart' hide SelectionHandleDriver;
26 27
 export 'src/widgets/theme.dart';
27 28
 export 'src/widgets/toolbar.dart';
29
+export 'src/widgets/view.dart';

+ 4
- 4
packages/zefyr/test/rendering/render_editable_paragraph_test.dart View File

@@ -11,20 +11,20 @@ import 'package:zefyr/src/widgets/rich_text.dart';
11 11
 import 'package:zefyr/zefyr.dart';
12 12
 
13 13
 void main() {
14
-  group('$RenderEditableParagraph', () {
14
+  group('$RenderZefyrParagraph', () {
15 15
     final doc = new NotusDocument();
16 16
     doc.insert(0, 'This House Is A Circus');
17 17
     final text = new TextSpan(text: 'This House Is A Circus');
18 18
 
19 19
     ZefyrRenderContext renderContext;
20
-    RenderEditableParagraph p;
20
+    RenderZefyrParagraph p;
21 21
 
22 22
     setUp(() {
23 23
       WidgetsFlutterBinding.ensureInitialized();
24 24
       renderContext = new ZefyrRenderContext();
25
-      p = new RenderEditableParagraph(
25
+      p = new RenderZefyrParagraph(
26 26
         text,
27
-        node: doc.root.children.first,
27
+        node: doc.root.children.first as LineNode,
28 28
         textDirection: TextDirection.ltr,
29 29
       );
30 30
     });

+ 2
- 2
packages/zefyr/test/widgets/buttons_test.dart View File

@@ -24,7 +24,7 @@ void main() {
24 24
       expect(bold.value, 'House');
25 25
 
26 26
       await editor.tapButtonWithIcon(Icons.format_bold);
27
-      line = editor.document.root.children.first;
27
+      line = editor.document.root.children.first as LineNode;
28 28
       expect(line.childCount, 1);
29 29
     });
30 30
 
@@ -139,7 +139,7 @@ void main() {
139 139
       await editor.updateSelection(base: 7, extent: 7);
140 140
       await editor.tapButtonWithIcon(Icons.link);
141 141
       await editor.tapButtonWithIcon(Icons.link_off);
142
-      line = editor.document.root.children.first;
142
+      line = editor.document.root.children.first as LineNode;
143 143
       expect(line.childCount, 1);
144 144
     });
145 145
   });

+ 0
- 107
packages/zefyr/test/widgets/editable_text_scope_test.dart View File

@@ -1,107 +0,0 @@
1
-// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
-// for details. All rights reserved. Use of this source code is governed by a
3
-// BSD-style license that can be found in the LICENSE file.
4
-
5
-import 'package:flutter/material.dart';
6
-import 'package:flutter_test/flutter_test.dart';
7
-import 'package:zefyr/src/widgets/editable_box.dart';
8
-import 'package:zefyr/src/widgets/render_context.dart';
9
-import 'package:zefyr/zefyr.dart';
10
-
11
-void main() {
12
-  group('$ZefyrEditableTextScope', () {
13
-    setUp(() {
14
-      WidgetsFlutterBinding.ensureInitialized();
15
-    });
16
-
17
-    test('updateShouldNotify for rendering context changes', () {
18
-      var context = new ZefyrRenderContext();
19
-      var paragraph1 = createParagraph(context);
20
-      var paragraph2 = createParagraph(context);
21
-      context.addBox(paragraph1);
22
-      context.markDirty(paragraph1, false);
23
-      var widget1 = createScope(renderingContext: context);
24
-      var widget2 = createScope(renderingContext: context);
25
-
26
-      expect(widget2.updateShouldNotify(widget1), isFalse);
27
-      context.addBox(paragraph2);
28
-      context.markDirty(paragraph2, false);
29
-      widget2 = createScope(renderingContext: context);
30
-      expect(widget2.updateShouldNotify(widget1), isTrue);
31
-    });
32
-
33
-    test('updateShouldNotify for selection changes', () {
34
-      var context = new ZefyrRenderContext();
35
-      var selection = new TextSelection.collapsed(offset: 0);
36
-      var widget1 =
37
-          createScope(renderingContext: context, selection: selection);
38
-      var widget2 =
39
-          createScope(renderingContext: context, selection: selection);
40
-
41
-      expect(widget2.updateShouldNotify(widget1), isFalse);
42
-      selection = new TextSelection.collapsed(offset: 1);
43
-      widget2 = createScope(renderingContext: context, selection: selection);
44
-      expect(widget2.updateShouldNotify(widget1), isTrue);
45
-    });
46
-
47
-    test('updateShouldNotify for showCursor changes', () {
48
-      var context = new ZefyrRenderContext();
49
-      var showCursor = new ValueNotifier<bool>(true);
50
-      var widget1 =
51
-          createScope(renderingContext: context, showCursor: showCursor);
52
-      var widget2 =
53
-          createScope(renderingContext: context, showCursor: showCursor);
54
-
55
-      expect(widget2.updateShouldNotify(widget1), isFalse);
56
-      showCursor = new ValueNotifier<bool>(true);
57
-      widget2 = createScope(renderingContext: context, showCursor: showCursor);
58
-      expect(widget2.updateShouldNotify(widget1), isTrue);
59
-    });
60
-
61
-    test('updateShouldNotify for imageDelegate changes', () {
62
-      var context = new ZefyrRenderContext();
63
-      var delegate = new ZefyrDefaultImageDelegate();
64
-      var widget1 =
65
-          createScope(renderingContext: context, imageDelegate: delegate);
66
-      var widget2 =
67
-          createScope(renderingContext: context, imageDelegate: delegate);
68
-
69
-      expect(widget2.updateShouldNotify(widget1), isFalse);
70
-      delegate = new ZefyrDefaultImageDelegate();
71
-      widget2 = createScope(renderingContext: context, imageDelegate: delegate);
72
-      expect(widget2.updateShouldNotify(widget1), isTrue);
73
-    });
74
-  });
75
-}
76
-
77
-ZefyrEditableTextScope createScope({
78
-  @required ZefyrRenderContext renderingContext,
79
-  TextSelection selection,
80
-  ValueNotifier<bool> showCursor,
81
-  ZefyrImageDelegate imageDelegate,
82
-}) {
83
-  return ZefyrEditableTextScope(
84
-    renderContext: renderingContext,
85
-    selection: selection,
86
-    showCursor: showCursor,
87
-    imageDelegate: imageDelegate,
88
-    child: null,
89
-  );
90
-}
91
-
92
-RenderEditableProxyBox createParagraph(ZefyrRenderContext context) {
93
-  final doc = new NotusDocument();
94
-  doc.insert(0, 'This House Is A Circus');
95
-  final link = new LayerLink();
96
-  final showCursor = new ValueNotifier<bool>(true);
97
-  final selection = new TextSelection.collapsed(offset: 0);
98
-  final selectionColor = Colors.blue;
99
-  return new RenderEditableProxyBox(
100
-    node: doc.root.children.first,
101
-    layerLink: link,
102
-    renderContext: context,
103
-    showCursor: showCursor,
104
-    selection: selection,
105
-    selectionColor: selectionColor,
106
-  );
107
-}

+ 1
- 1
packages/zefyr/test/widgets/editor_test.dart View File

@@ -25,7 +25,7 @@ void main() {
25 25
       await editor.tapEditor();
26 26
       // TODO: figure out why this extra pump is needed here
27 27
       await tester.pumpAndSettle();
28
-      EditableRichText p = tester.widget(find.byType(EditableRichText).first);
28
+      ZefyrRichText p = tester.widget(find.byType(ZefyrRichText).first);
29 29
       expect(p.text.children.first.style.color, Colors.red);
30 30
     });
31 31
 

+ 1
- 0
packages/zefyr/test/widgets/image_test.dart View File

@@ -105,6 +105,7 @@ void main() {
105 105
       EmbedNode embed = line.children.single;
106 106
       expect(editor.selection.baseOffset, embed.documentOffset);
107 107
       expect(editor.selection.extentOffset, embed.documentOffset + 1);
108
+      expect(find.text('Paste'), findsOneWidget);
108 109
     });
109 110
   });
110 111
 }

+ 5
- 5
packages/zefyr/test/widgets/rich_text_test.dart View File

@@ -9,7 +9,7 @@ import 'package:zefyr/src/widgets/rich_text.dart';
9 9
 import 'package:zefyr/zefyr.dart';
10 10
 
11 11
 void main() {
12
-  group('$EditableRichText', () {
12
+  group('$ZefyrRichText', () {
13 13
     final doc = new NotusDocument();
14 14
     doc.insert(0, 'This House Is A Circus');
15 15
     final text = new TextSpan(text: 'This House Is A Circus');
@@ -18,8 +18,8 @@ void main() {
18 18
     setUp(() {
19 19
       widget = new Directionality(
20 20
         textDirection: TextDirection.ltr,
21
-        child: new EditableRichText(
22
-          node: doc.root.children.first,
21
+        child: new ZefyrRichText(
22
+          node: doc.root.children.first as LineNode,
23 23
           text: text,
24 24
         ),
25 25
       );
@@ -27,8 +27,8 @@ void main() {
27 27
 
28 28
     testWidgets('initialize', (tester) async {
29 29
       await tester.pumpWidget(widget);
30
-      EditableRichText result =
31
-          tester.firstWidget(find.byType(EditableRichText));
30
+      ZefyrRichText result =
31
+          tester.firstWidget(find.byType(ZefyrRichText));
32 32
       expect(result, isNotNull);
33 33
       expect(result.text.text, 'This House Is A Circus');
34 34
     });

+ 75
- 0
packages/zefyr/test/widgets/scope_test.dart View File

@@ -0,0 +1,75 @@
1
+// Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
+// for details. All rights reserved. Use of this source code is governed by a
3
+// BSD-style license that can be found in the LICENSE file.
4
+
5
+import 'package:flutter/material.dart';
6
+import 'package:flutter_test/flutter_test.dart';
7
+import 'package:zefyr/zefyr.dart';
8
+
9
+void main() {
10
+  group('$ZefyrScope', () {
11
+    ZefyrScope scope;
12
+
13
+    setUp(() {
14
+      WidgetsFlutterBinding.ensureInitialized();
15
+      final doc = NotusDocument();
16
+      scope = ZefyrScope.editable(
17
+        controller: ZefyrController(doc),
18
+        imageDelegate: ZefyrDefaultImageDelegate(),
19
+        focusNode: FocusNode(),
20
+        focusScope: FocusScopeNode(),
21
+      );
22
+    });
23
+
24
+    test('it notifies on image delegate update', () {
25
+      bool notified = false;
26
+      scope.addListener(() {
27
+        notified = true;
28
+      });
29
+      final delegate = ZefyrDefaultImageDelegate();
30
+      scope.imageDelegate = delegate;
31
+      expect(notified, isTrue);
32
+      notified = false;
33
+      scope.imageDelegate = delegate;
34
+      expect(notified, isFalse);
35
+    });
36
+
37
+    test('it notifies on controller update', () {
38
+      bool notified = false;
39
+      scope.addListener(() {
40
+        notified = true;
41
+      });
42
+      final controller = ZefyrController(NotusDocument());
43
+      scope.controller = controller;
44
+      expect(notified, isTrue);
45
+      notified = false;
46
+      scope.controller = controller;
47
+      expect(notified, isFalse);
48
+    });
49
+
50
+    test('it notifies on focus node update', () {
51
+      bool notified = false;
52
+      scope.addListener(() {
53
+        notified = true;
54
+      });
55
+      final focusNode = FocusNode();
56
+      scope.focusNode = focusNode;
57
+      expect(notified, isTrue);
58
+      notified = false;
59
+      scope.focusNode = focusNode;
60
+      expect(notified, isFalse);
61
+    });
62
+
63
+    test('it notifies on selection changes but not text changes', () {
64
+      bool notified = false;
65
+      scope.addListener(() {
66
+        notified = true;
67
+      });
68
+
69
+      scope.controller.replaceText(0, 0, 'Hello');
70
+      expect(notified, isFalse);
71
+      scope.controller.updateSelection(TextSelection.collapsed(offset: 4));
72
+      expect(notified, isTrue);
73
+    });
74
+  });
75
+}

+ 4
- 4
packages/zefyr/test/widgets/selection_test.dart View File

@@ -14,8 +14,8 @@ void main() {
14 14
       final editor = new EditorSandBox(tester: tester);
15 15
       await editor.tapEditor();
16 16
 
17
-      RenderEditableParagraph renderObject =
18
-          tester.firstRenderObject(find.byType(EditableRichText));
17
+      RenderZefyrParagraph renderObject =
18
+          tester.firstRenderObject(find.byType(ZefyrRichText));
19 19
       var offset = renderObject.localToGlobal(Offset.zero);
20 20
       offset += Offset(5.0, 5.0);
21 21
       await tester.tapAt(offset);
@@ -42,8 +42,8 @@ void main() {
42 42
       await tester.pumpAndSettle();
43 43
       expect(editor.controller.selection.extentOffset, 10);
44 44
 
45
-      RenderEditableParagraph renderObject =
46
-          tester.firstRenderObject(find.byType(EditableRichText));
45
+      RenderZefyrParagraph renderObject =
46
+          tester.firstRenderObject(find.byType(ZefyrRichText));
47 47
       var offset = renderObject.localToGlobal(Offset.zero);
48 48
       offset += Offset(-5.0, 5.0);
49 49
       await tester.tapAt(offset);