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
 
12
 
13
 ```yaml
13
 ```yaml
14
 dependencies:
14
 dependencies:
15
-  zefyr: ^0.3.0
15
+  zefyr: [latest_version]
16
 ```
16
 ```
17
 
17
 
18
 And run `flutter packages get` to install. This installs both `zefyr`
18
 And run `flutter packages get` to install. This installs both `zefyr`

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

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

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

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

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

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

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

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

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

40
       while (child != line) {
40
       while (child != line) {
41
         child.unlink();
41
         child.unlink();
42
         before.add(child);
42
         before.add(child);
43
-        child = this.first;
43
+        child = this.first as LineNode;
44
       }
44
       }
45
       line.unlink();
45
       line.unlink();
46
       insertBefore(line);
46
       insertBefore(line);

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

52
     assert(index >= 0 && index <= length);
52
     assert(index >= 0 && index <= length);
53
     if (index == 0) return this;
53
     if (index == 0) return this;
54
     if (index == length && isLast) return null;
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
     String text = _value;
57
     String text = _value;
58
     _value = text.substring(0, index);
58
     _value = text.substring(0, index);
124
   }
124
   }
125
 
125
 
126
   @override
126
   @override
127
-  LineNode get parent => super.parent;
127
+  LineNode get parent => super.parent as LineNode;
128
 
128
 
129
   @override
129
   @override
130
   int get length => _value.length;
130
   int get length => _value.length;

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

34
     if (isLast) {
34
     if (isLast) {
35
       if (parent is BlockNode) {
35
       if (parent is BlockNode) {
36
         if (parent.isLast) return null;
36
         if (parent.isLast) return null;
37
-        return (parent.next is BlockNode)
37
+        LineNode line = (parent.next is BlockNode)
38
             ? (parent.next as BlockNode).first
38
             ? (parent.next as BlockNode).first
39
             : parent.next;
39
             : parent.next;
40
+        return line;
40
       } else
41
       } else
41
         return null;
42
         return null;
42
     } else {
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
       result = result.mergeAll(node.style);
131
       result = result.mergeAll(node.style);
130
       int pos = node.length - data.offset;
132
       int pos = node.length - data.offset;
131
       while (!node.isLast && pos < local) {
133
       while (!node.isLast && pos < local) {
132
-        node = node.next;
134
+        node = node.next as LeafNode;
133
         _handle(node.style);
135
         _handle(node.style);
134
         pos += node.length;
136
         pos += node.length;
135
       }
137
       }

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

238
 
238
 
239
     if (isEmpty) {
239
     if (isEmpty) {
240
       assert(index == 0);
240
       assert(index == 0);
241
-      Node node = defaultChild;
241
+      T node = defaultChild;
242
       add(node);
242
       add(node);
243
       node.insert(index, value, style);
243
       node.insert(index, value, style);
244
     } else {
244
     } else {

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

69
 
69
 
70
   Map<String, dynamic> _unsetAttributes(Map<String, dynamic> attributes) {
70
   Map<String, dynamic> _unsetAttributes(Map<String, dynamic> attributes) {
71
     if (attributes == null) return null;
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
     final target = iter.next();
87
     final target = iter.next();
88
 
88
 
89
     if (target.data.startsWith('\n')) {
89
     if (target.data.startsWith('\n')) {
90
-      Map<String, dynamic> resetStyle = null;
90
+      Map<String, dynamic> resetStyle;
91
       if (target.attributes != null &&
91
       if (target.attributes != null &&
92
           target.attributes.containsKey(NotusAttribute.heading.key)) {
92
           target.attributes.containsKey(NotusAttribute.heading.key)) {
93
         resetStyle = NotusAttribute.heading.unset.toJson();
93
         resetStyle = NotusAttribute.heading.unset.toJson();
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
     if (lineStyle != null) {
295
     if (lineStyle != null) {
296
       if (lineStyle.containsKey(NotusAttribute.heading.key)) {
296
       if (lineStyle.containsKey(NotusAttribute.heading.key)) {
297
         resetStyle = NotusAttribute.heading.unset.toJson();
297
         resetStyle = NotusAttribute.heading.unset.toJson();

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

141
 
141
 
142
 final doc =
142
 final doc =
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"}}]';
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
 final expectedMarkdown = '''
146
 final expectedMarkdown = '''
147
 # Zefyr
147
 # Zefyr

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

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

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

46
     test('json serialization', () {
46
     test('json serialization', () {
47
       final original = dartconfDoc();
47
       final original = dartconfDoc();
48
       final jsonData = json.encode(original);
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
       expect(doc.toDelta(), original.toDelta());
50
       expect(doc.toDelta(), original.toDelta());
51
       expect(json.encode(doc), jsonData);
51
       expect(json.encode(doc), jsonData);
52
     });
52
     });

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

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
 ## 0.3.0
14
 ## 0.3.0
2
 
15
 
3
 This version introduces new widget `ZefyrScaffold` which allows embedding Zefyr in custom
16
 This version introduces new widget `ZefyrScaffold` which allows embedding Zefyr in custom

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

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

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

4
 import 'package:flutter/material.dart';
4
 import 'package:flutter/material.dart';
5
 import 'src/form.dart';
5
 import 'src/form.dart';
6
 import 'src/full_page.dart';
6
 import 'src/full_page.dart';
7
+import 'src/view.dart';
7
 
8
 
8
 void main() {
9
 void main() {
9
   runApp(new ZefyrApp());
10
   runApp(new ZefyrApp());
20
       routes: {
21
       routes: {
21
         "/fullPage": buildFullPage,
22
         "/fullPage": buildFullPage,
22
         "/form": buildFormPage,
23
         "/form": buildFormPage,
24
+        "/view": buildViewPage,
23
       },
25
       },
24
     );
26
     );
25
   }
27
   }
31
   Widget buildFormPage(BuildContext context) {
33
   Widget buildFormPage(BuildContext context) {
32
     return FormEmbeddedScreen();
34
     return FormEmbeddedScreen();
33
   }
35
   }
36
+
37
+  Widget buildViewPage(BuildContext context) {
38
+    return ViewScreen();
39
+  }
34
 }
40
 }
35
 
41
 
36
 class HomePage extends StatelessWidget {
42
 class HomePage extends StatelessWidget {
59
             color: Colors.lightBlue,
65
             color: Colors.lightBlue,
60
             textColor: Colors.white,
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
           Expanded(child: Container()),
74
           Expanded(child: Container()),
63
         ],
75
         ],
64
       ),
76
       ),

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

1
+import 'dart:async';
1
 import 'dart:convert';
2
 import 'dart:convert';
2
 
3
 
3
 import 'package:flutter/material.dart';
4
 import 'package:flutter/material.dart';
31
     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
     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
 Delta getDelta() {
34
 Delta getDelta() {
34
-  return Delta.fromJson(json.decode(doc));
35
+  return Delta.fromJson(json.decode(doc) as List);
35
 }
36
 }
36
 
37
 
37
 class _FullPageEditorScreenState extends State<FullPageEditorScreen> {
38
 class _FullPageEditorScreenState extends State<FullPageEditorScreen> {
39
       ZefyrController(NotusDocument.fromDelta(getDelta()));
40
       ZefyrController(NotusDocument.fromDelta(getDelta()));
40
   final FocusNode _focusNode = new FocusNode();
41
   final FocusNode _focusNode = new FocusNode();
41
   bool _editing = false;
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
   @override
59
   @override
44
   Widget build(BuildContext context) {
60
   Widget build(BuildContext context) {

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

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

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

6
 import 'package:notus/notus.dart';
6
 import 'package:notus/notus.dart';
7
 
7
 
8
 import 'editable_box.dart';
8
 import 'editable_box.dart';
9
-import 'editable_text.dart';
10
 import 'horizontal_rule.dart';
9
 import 'horizontal_rule.dart';
11
 import 'image.dart';
10
 import 'image.dart';
12
 import 'rich_text.dart';
11
 import 'rich_text.dart';
12
+import 'scope.dart';
13
 import 'theme.dart';
13
 import 'theme.dart';
14
 
14
 
15
 /// Raw widget representing a single line of rich text document in Zefyr editor.
15
 /// Raw widget representing a single line of rich text document in Zefyr editor.
43
 
43
 
44
   @override
44
   @override
45
   Widget build(BuildContext context) {
45
   Widget build(BuildContext context) {
46
-    ensureVisible(context);
46
+    final scope = ZefyrScope.of(context);
47
+    if (scope.isEditable) {
48
+      ensureVisible(context, scope);
49
+    }
47
     final theme = ZefyrTheme.of(context);
50
     final theme = ZefyrTheme.of(context);
48
-    final editable = ZefyrEditableText.of(context);
49
 
51
 
50
     Widget content;
52
     Widget content;
51
     if (widget.node.hasEmbed) {
53
     if (widget.node.hasEmbed) {
52
-      content = buildEmbed(context);
54
+      content = buildEmbed(context, scope);
53
     } else {
55
     } else {
54
       assert(widget.style != null);
56
       assert(widget.style != null);
55
-
56
-      final text = new EditableRichText(
57
+      content = ZefyrRichText(
57
         node: widget.node,
58
         node: widget.node,
58
         text: buildText(context),
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
         node: widget.node,
66
         node: widget.node,
63
         layerLink: _link,
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
         selectionColor: theme.selectionColor,
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
     if (widget.padding != null) {
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
       WidgetsBinding.instance.addPostFrameCallback((_) {
85
       WidgetsBinding.instance.addPostFrameCallback((_) {
83
         bringIntoView(context);
86
         bringIntoView(context);
84
       });
87
       });
136
     return result;
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
     EmbedNode node = widget.node.children.single;
143
     EmbedNode node = widget.node.children.single;
144
     EmbedAttribute embed = node.style.get(NotusAttribute.embed);
144
     EmbedAttribute embed = node.style.get(NotusAttribute.embed);
145
 
145
 
146
     if (embed.type == EmbedType.horizontalRule) {
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
     } else if (embed.type == EmbedType.image) {
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
     } else {
150
     } else {
168
       throw new UnimplementedError('Unimplemented embed type ${embed.type}');
151
       throw new UnimplementedError('Unimplemented embed type ${embed.type}');
169
     }
152
     }

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

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

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

1
 // Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
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
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.
3
 // BSD-style license that can be found in the LICENSE file.
4
-import 'dart:async';
5
-
6
-import 'package:collection/collection.dart';
7
 import 'package:flutter/cupertino.dart';
4
 import 'package:flutter/cupertino.dart';
8
 import 'package:flutter/widgets.dart';
5
 import 'package:flutter/widgets.dart';
9
 import 'package:notus/notus.dart';
6
 import 'package:notus/notus.dart';
11
 import 'code.dart';
8
 import 'code.dart';
12
 import 'common.dart';
9
 import 'common.dart';
13
 import 'controller.dart';
10
 import 'controller.dart';
14
-import 'editable_box.dart';
11
+import 'cursor_timer.dart';
15
 import 'editor.dart';
12
 import 'editor.dart';
16
 import 'image.dart';
13
 import 'image.dart';
17
 import 'input.dart';
14
 import 'input.dart';
19
 import 'paragraph.dart';
16
 import 'paragraph.dart';
20
 import 'quote.dart';
17
 import 'quote.dart';
21
 import 'render_context.dart';
18
 import 'render_context.dart';
19
+import 'scope.dart';
22
 import 'selection.dart';
20
 import 'selection.dart';
21
+import 'theme.dart';
23
 
22
 
24
 /// Core widget responsible for editing Zefyr documents.
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
 /// Consider using [ZefyrEditor] which wraps this widget and adds a toolbar to
28
 /// Consider using [ZefyrEditor] which wraps this widget and adds a toolbar to
29
 /// edit style attributes.
29
 /// edit style attributes.
49
   /// Padding around editable area.
49
   /// Padding around editable area.
50
   final EdgeInsets padding;
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
   @override
52
   @override
59
   _ZefyrEditableTextState createState() => new _ZefyrEditableTextState();
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
 class _ZefyrEditableTextState extends State<ZefyrEditableText>
56
 class _ZefyrEditableTextState extends State<ZefyrEditableText>
92
     with AutomaticKeepAliveClientMixin {
57
     with AutomaticKeepAliveClientMixin {
93
   //
58
   //
102
 
67
 
103
   /// Current text selection.
68
   /// Current text selection.
104
   TextSelection get selection => widget.controller.selection;
69
   TextSelection get selection => widget.controller.selection;
105
-  ZefyrRenderContext get renderContext => _renderContext;
106
-  ValueNotifier<bool> get showCursor => _cursorTimer.value;
107
 
70
 
108
   /// Express interest in interacting with the keyboard.
71
   /// Express interest in interacting with the keyboard.
109
   ///
72
   ///
127
   Widget build(BuildContext context) {
90
   Widget build(BuildContext context) {
128
     FocusScope.of(context).reparentIfNeeded(focusNode);
91
     FocusScope.of(context).reparentIfNeeded(focusNode);
129
     super.build(context); // See AutomaticKeepAliveState.
92
     super.build(context); // See AutomaticKeepAliveState.
130
-    ZefyrEditor.of(context);
131
 
93
 
132
     Widget body = ListBody(children: _buildChildren(context));
94
     Widget body = ListBody(children: _buildChildren(context));
133
     if (widget.padding != null) {
95
     if (widget.padding != null) {
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
   @override
117
   @override
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
   @override
154
   @override
183
   void dispose() {
155
   void dispose() {
184
     _cancelSubscriptions();
156
     _cancelSubscriptions();
196
   // Private members
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
   InputConnectionController _input;
174
   InputConnectionController _input;
203
   bool _didAutoFocus = false;
175
   bool _didAutoFocus = false;
204
 
176
 
223
     final BlockNode block = node;
195
     final BlockNode block = node;
224
     final blockStyle = block.style.get(NotusAttribute.block);
196
     final blockStyle = block.style.get(NotusAttribute.block);
225
     if (blockStyle == NotusAttribute.block.code) {
197
     if (blockStyle == NotusAttribute.block.code) {
226
-      return new ZefyrCode(node: node);
198
+      return new ZefyrCode(node: block);
227
     } else if (blockStyle == NotusAttribute.block.bulletList) {
199
     } else if (blockStyle == NotusAttribute.block.bulletList) {
228
-      return new ZefyrList(node: node);
200
+      return new ZefyrList(node: block);
229
     } else if (blockStyle == NotusAttribute.block.numberList) {
201
     } else if (blockStyle == NotusAttribute.block.numberList) {
230
-      return new ZefyrList(node: node);
202
+      return new ZefyrList(node: block);
231
     } else if (blockStyle == NotusAttribute.block.quote) {
203
     } else if (blockStyle == NotusAttribute.block.quote) {
232
-      return new ZefyrQuote(node: node);
204
+      return new ZefyrQuote(node: block);
233
     }
205
     }
234
 
206
 
235
     throw new UnimplementedError('Block format $blockStyle.');
207
     throw new UnimplementedError('Block format $blockStyle.');
237
 
209
 
238
   void _updateSubscriptions([ZefyrEditableText oldWidget]) {
210
   void _updateSubscriptions([ZefyrEditableText oldWidget]) {
239
     if (oldWidget == null) {
211
     if (oldWidget == null) {
240
-      _renderContext.addListener(_handleRenderContextChange);
241
       widget.controller.addListener(_handleLocalValueChange);
212
       widget.controller.addListener(_handleLocalValueChange);
242
       focusNode.addListener(_handleFocusChange);
213
       focusNode.addListener(_handleFocusChange);
243
       return;
214
       return;
257
 
228
 
258
   void _cancelSubscriptions() {
229
   void _cancelSubscriptions() {
259
     _renderContext.removeListener(_handleRenderContextChange);
230
     _renderContext.removeListener(_handleRenderContextChange);
260
-    _renderContext.dispose();
261
     widget.controller.removeListener(_handleLocalValueChange);
231
     widget.controller.removeListener(_handleLocalValueChange);
262
     focusNode.removeListener(_handleFocusChange);
232
     focusNode.removeListener(_handleFocusChange);
263
     _input.closeConnection();
233
     _input.closeConnection();
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
 // for details. All rights reserved. Use of this source code is governed by a
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.
3
 // BSD-style license that can be found in the LICENSE file.
4
 import 'package:flutter/widgets.dart';
4
 import 'package:flutter/widgets.dart';
5
-import 'package:notus/notus.dart';
6
 
5
 
7
 import 'controller.dart';
6
 import 'controller.dart';
8
 import 'editable_text.dart';
7
 import 'editable_text.dart';
9
 import 'image.dart';
8
 import 'image.dart';
10
 import 'scaffold.dart';
9
 import 'scaffold.dart';
10
+import 'scope.dart';
11
 import 'theme.dart';
11
 import 'theme.dart';
12
 import 'toolbar.dart';
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
 /// Widget for editing Zefyr documents.
14
 /// Widget for editing Zefyr documents.
162
 class ZefyrEditor extends StatefulWidget {
15
 class ZefyrEditor extends StatefulWidget {
163
   const ZefyrEditor({
16
   const ZefyrEditor({
183
   /// Padding around editable area.
36
   /// Padding around editable area.
184
   final EdgeInsets padding;
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
   @override
39
   @override
193
   _ZefyrEditorState createState() => new _ZefyrEditorState();
40
   _ZefyrEditorState createState() => new _ZefyrEditorState();
194
 }
41
 }
195
 
42
 
196
 class _ZefyrEditorState extends State<ZefyrEditor> {
43
 class _ZefyrEditorState extends State<ZefyrEditor> {
197
   ZefyrImageDelegate _imageDelegate;
44
   ZefyrImageDelegate _imageDelegate;
198
-  ZefyrEditorScope _scope;
45
+  ZefyrScope _scope;
199
   ZefyrThemeData _themeData;
46
   ZefyrThemeData _themeData;
200
   GlobalKey<ZefyrToolbarState> _toolbarKey;
47
   GlobalKey<ZefyrToolbarState> _toolbarKey;
201
   ZefyrScaffoldState _scaffold;
48
   ZefyrScaffoldState _scaffold;
214
     _toolbarKey = null;
61
     _toolbarKey = null;
215
   }
62
   }
216
 
63
 
217
-  Widget buildToolbar(BuildContext) {
64
+  Widget buildToolbar(BuildContext context) {
218
     return ZefyrTheme(
65
     return ZefyrTheme(
219
       data: _themeData,
66
       data: _themeData,
220
       child: ZefyrToolbar(
67
       child: ZefyrToolbar(
231
     } else if (!hasToolbar) {
78
     } else if (!hasToolbar) {
232
       showToolbar();
79
       showToolbar();
233
     } else {
80
     } else {
81
+      // TODO: is there a nicer way to do this?
234
       WidgetsBinding.instance.addPostFrameCallback((_) {
82
       WidgetsBinding.instance.addPostFrameCallback((_) {
235
         _toolbarKey?.currentState?.markNeedsRebuild();
83
         _toolbarKey?.currentState?.markNeedsRebuild();
236
       });
84
       });
246
   @override
94
   @override
247
   void didUpdateWidget(ZefyrEditor oldWidget) {
95
   void didUpdateWidget(ZefyrEditor oldWidget) {
248
     super.didUpdateWidget(oldWidget);
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
     if (widget.imageDelegate != oldWidget.imageDelegate) {
99
     if (widget.imageDelegate != oldWidget.imageDelegate) {
252
       _imageDelegate = widget.imageDelegate ?? new ZefyrDefaultImageDelegate();
100
       _imageDelegate = widget.imageDelegate ?? new ZefyrDefaultImageDelegate();
253
-      _scope._updateImageDelegateIfNeeded(_imageDelegate);
101
+      _scope.imageDelegate = _imageDelegate;
254
     }
102
     }
255
   }
103
   }
256
 
104
 
264
         : fallbackTheme;
112
         : fallbackTheme;
265
 
113
 
266
     if (_scope == null) {
114
     if (_scope == null) {
267
-      _scope = ZefyrEditorScope(
115
+      _scope = ZefyrScope.editable(
268
         imageDelegate: _imageDelegate,
116
         imageDelegate: _imageDelegate,
269
         controller: widget.controller,
117
         controller: widget.controller,
270
         focusNode: widget.focusNode,
118
         focusNode: widget.focusNode,
273
       _scope.addListener(_handleChange);
121
       _scope.addListener(_handleChange);
274
     } else {
122
     } else {
275
       final focusScope = FocusScope.of(context);
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
     final scaffold = ZefyrScaffold.of(context);
127
     final scaffold = ZefyrScaffold.of(context);
298
   @override
144
   @override
299
   Widget build(BuildContext context) {
145
   Widget build(BuildContext context) {
300
     Widget editable = new ZefyrEditableText(
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
       autofocus: widget.autofocus,
150
       autofocus: widget.autofocus,
305
       enabled: widget.enabled,
151
       enabled: widget.enabled,
306
       padding: widget.padding,
152
       padding: widget.padding,
309
 
155
 
310
     return ZefyrTheme(
156
     return ZefyrTheme(
311
       data: _themeData,
157
       data: _themeData,
312
-      child: _ZefyrEditorScope(
158
+      child: ZefyrScopeAccess(
313
         scope: _scope,
159
         scope: _scope,
314
         child: editable,
160
         child: editable,
315
       ),
161
       ),

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

54
 class _ZefyrImageState extends State<ZefyrImage> {
54
 class _ZefyrImageState extends State<ZefyrImage> {
55
   String get imageSource {
55
   String get imageSource {
56
     EmbedAttribute attribute = widget.node.style.get(NotusAttribute.embed);
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
   @override
60
   @override

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

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

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

97
   /// Returns closest render box to the specified global [point].
97
   /// Returns closest render box to the specified global [point].
98
   ///
98
   ///
99
   /// If [point] is inside of one of active render boxes that box is returned.
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
   /// side of a box, e.g. if vertical offset of this point is inside of one of
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
   RenderEditableProxyBox closestBoxForGlobalPoint(Offset point) {
104
   RenderEditableProxyBox closestBoxForGlobalPoint(Offset point) {
104
     assert(!_disposed);
105
     assert(!_disposed);
105
     RenderEditableProxyBox box = boxForGlobalPoint(point);
106
     RenderEditableProxyBox box = boxForGlobalPoint(point);

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

11
 import 'caret.dart';
11
 import 'caret.dart';
12
 import 'editable_box.dart';
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
     @required this.node,
17
     @required this.node,
17
     @required this.text,
18
     @required this.text,
18
   }) : assert(node != null && text != null);
19
   }) : assert(node != null && text != null);
22
 
23
 
23
   @override
24
   @override
24
   RenderObject createRenderObject(BuildContext context) {
25
   RenderObject createRenderObject(BuildContext context) {
25
-    return new RenderEditableParagraph(
26
+    return new RenderZefyrParagraph(
26
       text,
27
       text,
27
       node: node,
28
       node: node,
28
       textDirection: Directionality.of(context),
29
       textDirection: Directionality.of(context),
31
 
32
 
32
   @override
33
   @override
33
   void updateRenderObject(
34
   void updateRenderObject(
34
-      BuildContext context, RenderEditableParagraph renderObject) {
35
+      BuildContext context, RenderZefyrParagraph renderObject) {
35
     renderObject
36
     renderObject
36
       ..text = text
37
       ..text = text
37
       ..node = node;
38
       ..node = node;
38
   }
39
   }
39
 }
40
 }
40
 
41
 
41
-class RenderEditableParagraph extends RenderParagraph
42
+class RenderZefyrParagraph extends RenderParagraph
42
     implements RenderEditableBox {
43
     implements RenderEditableBox {
43
-  RenderEditableParagraph(
44
+  RenderZefyrParagraph(
44
     TextSpan text, {
45
     TextSpan text, {
45
-    @required ContainerNode node,
46
+    @required LineNode node,
46
     TextAlign textAlign: TextAlign.start,
47
     TextAlign textAlign: TextAlign.start,
47
     @required TextDirection textDirection,
48
     @required TextDirection textDirection,
48
     bool softWrap: true,
49
     bool softWrap: true,

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

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

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

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

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

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
 export 'src/widgets/paragraph.dart';
22
 export 'src/widgets/paragraph.dart';
23
 export 'src/widgets/quote.dart';
23
 export 'src/widgets/quote.dart';
24
 export 'src/widgets/scaffold.dart';
24
 export 'src/widgets/scaffold.dart';
25
+export 'src/widgets/scope.dart' hide ZefyrScopeAccess;
25
 export 'src/widgets/selection.dart' hide SelectionHandleDriver;
26
 export 'src/widgets/selection.dart' hide SelectionHandleDriver;
26
 export 'src/widgets/theme.dart';
27
 export 'src/widgets/theme.dart';
27
 export 'src/widgets/toolbar.dart';
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
 import 'package:zefyr/zefyr.dart';
11
 import 'package:zefyr/zefyr.dart';
12
 
12
 
13
 void main() {
13
 void main() {
14
-  group('$RenderEditableParagraph', () {
14
+  group('$RenderZefyrParagraph', () {
15
     final doc = new NotusDocument();
15
     final doc = new NotusDocument();
16
     doc.insert(0, 'This House Is A Circus');
16
     doc.insert(0, 'This House Is A Circus');
17
     final text = new TextSpan(text: 'This House Is A Circus');
17
     final text = new TextSpan(text: 'This House Is A Circus');
18
 
18
 
19
     ZefyrRenderContext renderContext;
19
     ZefyrRenderContext renderContext;
20
-    RenderEditableParagraph p;
20
+    RenderZefyrParagraph p;
21
 
21
 
22
     setUp(() {
22
     setUp(() {
23
       WidgetsFlutterBinding.ensureInitialized();
23
       WidgetsFlutterBinding.ensureInitialized();
24
       renderContext = new ZefyrRenderContext();
24
       renderContext = new ZefyrRenderContext();
25
-      p = new RenderEditableParagraph(
25
+      p = new RenderZefyrParagraph(
26
         text,
26
         text,
27
-        node: doc.root.children.first,
27
+        node: doc.root.children.first as LineNode,
28
         textDirection: TextDirection.ltr,
28
         textDirection: TextDirection.ltr,
29
       );
29
       );
30
     });
30
     });

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

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

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

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
       await editor.tapEditor();
25
       await editor.tapEditor();
26
       // TODO: figure out why this extra pump is needed here
26
       // TODO: figure out why this extra pump is needed here
27
       await tester.pumpAndSettle();
27
       await tester.pumpAndSettle();
28
-      EditableRichText p = tester.widget(find.byType(EditableRichText).first);
28
+      ZefyrRichText p = tester.widget(find.byType(ZefyrRichText).first);
29
       expect(p.text.children.first.style.color, Colors.red);
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
       EmbedNode embed = line.children.single;
105
       EmbedNode embed = line.children.single;
106
       expect(editor.selection.baseOffset, embed.documentOffset);
106
       expect(editor.selection.baseOffset, embed.documentOffset);
107
       expect(editor.selection.extentOffset, embed.documentOffset + 1);
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
 import 'package:zefyr/zefyr.dart';
9
 import 'package:zefyr/zefyr.dart';
10
 
10
 
11
 void main() {
11
 void main() {
12
-  group('$EditableRichText', () {
12
+  group('$ZefyrRichText', () {
13
     final doc = new NotusDocument();
13
     final doc = new NotusDocument();
14
     doc.insert(0, 'This House Is A Circus');
14
     doc.insert(0, 'This House Is A Circus');
15
     final text = new TextSpan(text: 'This House Is A Circus');
15
     final text = new TextSpan(text: 'This House Is A Circus');
18
     setUp(() {
18
     setUp(() {
19
       widget = new Directionality(
19
       widget = new Directionality(
20
         textDirection: TextDirection.ltr,
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
           text: text,
23
           text: text,
24
         ),
24
         ),
25
       );
25
       );
27
 
27
 
28
     testWidgets('initialize', (tester) async {
28
     testWidgets('initialize', (tester) async {
29
       await tester.pumpWidget(widget);
29
       await tester.pumpWidget(widget);
30
-      EditableRichText result =
31
-          tester.firstWidget(find.byType(EditableRichText));
30
+      ZefyrRichText result =
31
+          tester.firstWidget(find.byType(ZefyrRichText));
32
       expect(result, isNotNull);
32
       expect(result, isNotNull);
33
       expect(result.text.text, 'This House Is A Circus');
33
       expect(result.text.text, 'This House Is A Circus');
34
     });
34
     });

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

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
       final editor = new EditorSandBox(tester: tester);
14
       final editor = new EditorSandBox(tester: tester);
15
       await editor.tapEditor();
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
       var offset = renderObject.localToGlobal(Offset.zero);
19
       var offset = renderObject.localToGlobal(Offset.zero);
20
       offset += Offset(5.0, 5.0);
20
       offset += Offset(5.0, 5.0);
21
       await tester.tapAt(offset);
21
       await tester.tapAt(offset);
42
       await tester.pumpAndSettle();
42
       await tester.pumpAndSettle();
43
       expect(editor.controller.selection.extentOffset, 10);
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
       var offset = renderObject.localToGlobal(Offset.zero);
47
       var offset = renderObject.localToGlobal(Offset.zero);
48
       offset += Offset(-5.0, 5.0);
48
       offset += Offset(-5.0, 5.0);
49
       await tester.tapAt(offset);
49
       await tester.tapAt(offset);