Sfoglia il codice sorgente

Theming layer rewrite and Dark Mode support (#232)

Anatoly Pulyaevskiy 5 anni fa
parent
commit
002d62476f
No account linked to committer's email address

+ 20
- 0
packages/zefyr/CHANGELOG.md Vedi File

@@ -1,3 +1,23 @@
1
+## 0.10.0
2
+
3
+This release contains breaking changes.
4
+The entire theming layer of ZefyrEditor has been rewritten. Most notable changes include:
5
+
6
+* Removed `selectionColor` and `cursorColor` from `ZefyrThemeData`. Relying on the Flutter 
7
+  `ThemeData.textSelectionColor` and `ThemeData.cursorColor` instead.
8
+* All attribute styles moved to the new `AttributeTheme` class.
9
+* `indentSize` renamed to `indentWidth`
10
+* Purpose of `BlockTheme` changed to specify styles for particular block type (list, quote, code)
11
+* Removed `HeadingTheme` and `StyleTheme`
12
+* Added new `LineTheme` to describe styles of headings and paragraphs
13
+
14
+Other changes in this release include:
15
+
16
+* Added: Support for Dark Mode
17
+* Changed: Minor tweaks to default theme
18
+* Fixed: ZefyrField decoration when focused appeared as disabled
19
+* Fixed: Caret color for iOS
20
+
1 21
 ## 0.9.1
2 22
 
3 23
 * Added: Support for iOS keyboard appearance. See `ZefyrEditor.keyboardAppearance` and `ZefyrField.keyboardAppearance`

+ 5
- 16
packages/zefyr/example/lib/main.dart Vedi File

@@ -2,6 +2,7 @@
2 2
 // for details. All rights reserved. Use of this source code is governed by a
3 3
 // BSD-style license that can be found in the LICENSE file.
4 4
 import 'package:flutter/material.dart';
5
+
5 6
 import 'src/form.dart';
6 7
 import 'src/full_page.dart';
7 8
 import 'src/view.dart';
@@ -16,7 +17,6 @@ class ZefyrApp extends StatelessWidget {
16 17
     return MaterialApp(
17 18
       debugShowCheckedModeBanner: false,
18 19
       title: 'Zefyr Editor',
19
-      theme: ThemeData(primarySwatch: Colors.cyan),
20 20
       home: HomePage(),
21 21
       routes: {
22 22
         "/fullPage": buildFullPage,
@@ -44,32 +44,21 @@ class HomePage extends StatelessWidget {
44 44
   Widget build(BuildContext context) {
45 45
     final nav = Navigator.of(context);
46 46
     return Scaffold(
47
-      appBar: AppBar(
48
-        elevation: 1.0,
49
-        backgroundColor: Colors.grey.shade200,
50
-        brightness: Brightness.light,
51
-        title: ZefyrLogo(),
52
-      ),
47
+      appBar: AppBar(title: ZefyrLogo()),
53 48
       body: Column(
54 49
         children: <Widget>[
55 50
           Expanded(child: Container()),
56
-          FlatButton(
51
+          RaisedButton(
57 52
             onPressed: () => nav.pushNamed('/fullPage'),
58 53
             child: Text('Full page editor'),
59
-            color: Colors.lightBlue,
60
-            textColor: Colors.white,
61 54
           ),
62
-          FlatButton(
55
+          RaisedButton(
63 56
             onPressed: () => nav.pushNamed('/form'),
64 57
             child: Text('Embedded in a form'),
65
-            color: Colors.lightBlue,
66
-            textColor: Colors.white,
67 58
           ),
68
-          FlatButton(
59
+          RaisedButton(
69 60
             onPressed: () => nav.pushNamed('/view'),
70 61
             child: Text('Read-only embeddable view'),
71
-            color: Colors.lightBlue,
72
-            textColor: Colors.white,
73 62
           ),
74 63
           Expanded(child: Container()),
75 64
         ],

+ 41
- 22
packages/zefyr/example/lib/src/form.dart Vedi File

@@ -8,6 +8,8 @@ import 'package:zefyr/zefyr.dart';
8 8
 import 'full_page.dart';
9 9
 import 'images.dart';
10 10
 
11
+enum _Options { darkTheme }
12
+
11 13
 class FormEmbeddedScreen extends StatefulWidget {
12 14
   @override
13 15
   _FormEmbeddedScreenState createState() => _FormEmbeddedScreenState();
@@ -17,6 +19,8 @@ class _FormEmbeddedScreenState extends State<FormEmbeddedScreen> {
17 19
   final ZefyrController _controller = ZefyrController(NotusDocument());
18 20
   final FocusNode _focusNode = FocusNode();
19 21
 
22
+  bool _darkTheme = false;
23
+
20 24
   @override
21 25
   Widget build(BuildContext context) {
22 26
     final form = ListView(
@@ -27,13 +31,16 @@ class _FormEmbeddedScreenState extends State<FormEmbeddedScreen> {
27 31
       ],
28 32
     );
29 33
 
30
-    return Scaffold(
34
+    final result = Scaffold(
31 35
       resizeToAvoidBottomPadding: true,
32 36
       appBar: AppBar(
33
-        elevation: 1.0,
34
-        backgroundColor: Colors.grey.shade200,
35
-        brightness: Brightness.light,
36 37
         title: ZefyrLogo(),
38
+        actions: [
39
+          PopupMenuButton<_Options>(
40
+            itemBuilder: buildPopupMenu,
41
+            onSelected: handlePopupItemSelected,
42
+          )
43
+        ],
37 44
       ),
38 45
       body: ZefyrScaffold(
39 46
         child: Padding(
@@ -42,29 +49,41 @@ class _FormEmbeddedScreenState extends State<FormEmbeddedScreen> {
42 49
         ),
43 50
       ),
44 51
     );
52
+
53
+    if (_darkTheme) {
54
+      return Theme(data: ThemeData.dark(), child: result);
55
+    }
56
+    return Theme(data: ThemeData(primarySwatch: Colors.cyan), child: result);
45 57
   }
46 58
 
47 59
   Widget buildEditor() {
48
-    final theme = ZefyrThemeData(
49
-      toolbarTheme: ZefyrToolbarTheme.fallback(context).copyWith(
50
-        color: Colors.grey.shade800,
51
-        toggleColor: Colors.grey.shade900,
52
-        iconColor: Colors.white,
53
-        disabledIconColor: Colors.grey.shade500,
54
-      ),
60
+    return ZefyrField(
61
+      height: 200.0,
62
+      decoration: InputDecoration(labelText: 'Description'),
63
+      controller: _controller,
64
+      focusNode: _focusNode,
65
+      autofocus: true,
66
+      imageDelegate: CustomImageDelegate(),
67
+      physics: ClampingScrollPhysics(),
55 68
     );
69
+  }
70
+
71
+  void handlePopupItemSelected(value) {
72
+    if (!mounted) return;
73
+    setState(() {
74
+      if (value == _Options.darkTheme) {
75
+        _darkTheme = !_darkTheme;
76
+      }
77
+    });
78
+  }
56 79
 
57
-    return ZefyrTheme(
58
-      data: theme,
59
-      child: ZefyrField(
60
-        height: 200.0,
61
-        decoration: InputDecoration(labelText: 'Description'),
62
-        controller: _controller,
63
-        focusNode: _focusNode,
64
-        autofocus: true,
65
-        imageDelegate: CustomImageDelegate(),
66
-        physics: ClampingScrollPhysics(),
80
+  List<PopupMenuEntry<_Options>> buildPopupMenu(BuildContext context) {
81
+    return [
82
+      CheckedPopupMenuItem(
83
+        value: _Options.darkTheme,
84
+        child: Text("Dark theme"),
85
+        checked: _darkTheme,
67 86
       ),
68
-    );
87
+    ];
69 88
   }
70 89
 }

+ 42
- 26
packages/zefyr/example/lib/src/full_page.dart Vedi File

@@ -41,12 +41,15 @@ Delta getDelta() {
41 41
   return Delta.fromJson(json.decode(doc) as List);
42 42
 }
43 43
 
44
+enum _Options { darkTheme }
45
+
44 46
 class _FullPageEditorScreenState extends State<FullPageEditorScreen> {
45 47
   final ZefyrController _controller =
46 48
       ZefyrController(NotusDocument.fromDelta(getDelta()));
47 49
   final FocusNode _focusNode = FocusNode();
48 50
   bool _editing = false;
49 51
   StreamSubscription<NotusChange> _sub;
52
+  bool _darkTheme = false;
50 53
 
51 54
   @override
52 55
   void initState() {
@@ -64,41 +67,54 @@ class _FullPageEditorScreenState extends State<FullPageEditorScreen> {
64 67
 
65 68
   @override
66 69
   Widget build(BuildContext context) {
67
-    final theme = ZefyrThemeData(
68
-      cursorColor: Colors.blue,
69
-      toolbarTheme: ZefyrToolbarTheme.fallback(context).copyWith(
70
-        color: Colors.grey.shade800,
71
-        toggleColor: Colors.grey.shade900,
72
-        iconColor: Colors.white,
73
-        disabledIconColor: Colors.grey.shade500,
74
-      ),
75
-    );
76
-
77 70
     final done = _editing
78
-        ? [FlatButton(onPressed: _stopEditing, child: Text('DONE'))]
79
-        : [FlatButton(onPressed: _startEditing, child: Text('EDIT'))];
80
-    return Scaffold(
71
+        ? IconButton(onPressed: _stopEditing, icon: Icon(Icons.save))
72
+        : IconButton(onPressed: _startEditing, icon: Icon(Icons.edit));
73
+    final result = Scaffold(
81 74
       resizeToAvoidBottomPadding: true,
82 75
       appBar: AppBar(
83
-        elevation: 1.0,
84
-        backgroundColor: Colors.grey.shade200,
85
-        brightness: Brightness.light,
86 76
         title: ZefyrLogo(),
87
-        actions: done,
77
+        actions: [
78
+          done,
79
+          PopupMenuButton<_Options>(
80
+            itemBuilder: buildPopupMenu,
81
+            onSelected: handlePopupItemSelected,
82
+          )
83
+        ],
88 84
       ),
89 85
       body: ZefyrScaffold(
90
-        child: ZefyrTheme(
91
-          data: theme,
92
-          child: ZefyrEditor(
93
-            controller: _controller,
94
-            focusNode: _focusNode,
95
-            mode: _editing ? ZefyrMode.edit : ZefyrMode.select,
96
-            imageDelegate: CustomImageDelegate(),
97
-            keyboardAppearance: Brightness.dark,
98
-          ),
86
+        child: ZefyrEditor(
87
+          controller: _controller,
88
+          focusNode: _focusNode,
89
+          mode: _editing ? ZefyrMode.edit : ZefyrMode.select,
90
+          imageDelegate: CustomImageDelegate(),
91
+          keyboardAppearance: _darkTheme ? Brightness.dark : Brightness.light,
99 92
         ),
100 93
       ),
101 94
     );
95
+    if (_darkTheme) {
96
+      return Theme(data: ThemeData.dark(), child: result);
97
+    }
98
+    return Theme(data: ThemeData(primarySwatch: Colors.cyan), child: result);
99
+  }
100
+
101
+  void handlePopupItemSelected(value) {
102
+    if (!mounted) return;
103
+    setState(() {
104
+      if (value == _Options.darkTheme) {
105
+        _darkTheme = !_darkTheme;
106
+      }
107
+    });
108
+  }
109
+
110
+  List<PopupMenuEntry<_Options>> buildPopupMenu(BuildContext context) {
111
+    return [
112
+      CheckedPopupMenuItem(
113
+        value: _Options.darkTheme,
114
+        child: Text("Dark theme"),
115
+        checked: _darkTheme,
116
+      ),
117
+    ];
102 118
   }
103 119
 
104 120
   void _startEditing() {

+ 18
- 34
packages/zefyr/example/lib/src/view.dart Vedi File

@@ -32,43 +32,27 @@ class _ViewScreen extends State<ViewScreen> {
32 32
 
33 33
   @override
34 34
   Widget build(BuildContext context) {
35
-    final theme = ZefyrThemeData(
36
-      toolbarTheme: ZefyrToolbarTheme.fallback(context).copyWith(
37
-        color: Colors.grey.shade800,
38
-        toggleColor: Colors.grey.shade900,
39
-        iconColor: Colors.white,
40
-        disabledIconColor: Colors.grey.shade500,
41
-      ),
42
-    );
43 35
     return Scaffold(
44 36
       resizeToAvoidBottomPadding: true,
45
-      appBar: AppBar(
46
-        elevation: 1.0,
47
-        backgroundColor: Colors.grey.shade200,
48
-        brightness: Brightness.light,
49
-        title: ZefyrLogo(),
50
-      ),
51
-      body: ZefyrTheme(
52
-        data: theme,
53
-        child: ListView(
54
-          children: <Widget>[
55
-            SizedBox(height: 16.0),
56
-            ListTile(
57
-              leading: Icon(Icons.info),
58
-              title: Text('ZefyrView inside ListView'),
59
-              subtitle: Text(
60
-                  'Allows embedding Notus documents in custom scrollables'),
61
-              trailing: Icon(Icons.keyboard_arrow_down),
37
+      appBar: AppBar(title: ZefyrLogo()),
38
+      body: ListView(
39
+        children: <Widget>[
40
+          SizedBox(height: 16.0),
41
+          ListTile(
42
+            leading: Icon(Icons.info),
43
+            title: Text('ZefyrView inside ListView'),
44
+            subtitle:
45
+                Text('Allows embedding Notus documents in custom scrollables'),
46
+            trailing: Icon(Icons.keyboard_arrow_down),
47
+          ),
48
+          Padding(
49
+            padding: const EdgeInsets.all(16.0),
50
+            child: ZefyrView(
51
+              document: doc,
52
+              imageDelegate: CustomImageDelegate(),
62 53
             ),
63
-            Padding(
64
-              padding: const EdgeInsets.all(16.0),
65
-              child: ZefyrView(
66
-                document: doc,
67
-                imageDelegate: CustomImageDelegate(),
68
-              ),
69
-            )
70
-          ],
71
-        ),
54
+          )
55
+        ],
72 56
       ),
73 57
     );
74 58
   }

+ 330
- 0
packages/zefyr/lib/src/widgets/__theme.dart Vedi File

@@ -0,0 +1,330 @@
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/widgets.dart';
7
+import 'package:meta/meta.dart';
8
+
9
+/// Applies a Zefyr editor theme to descendant widgets.
10
+///
11
+/// Describes colors and typographic styles for an editor.
12
+///
13
+/// Descendant widgets obtain the current theme's [ZefyrThemeData] object using
14
+/// [ZefyrTheme.of].
15
+///
16
+/// See also:
17
+///
18
+///   * [ZefyrThemeData], which describes actual configuration of a theme.
19
+class ZefyrTheme extends InheritedWidget {
20
+  final ZefyrThemeData data;
21
+
22
+  /// Applies the given theme [data] to [child].
23
+  ///
24
+  /// The [data] and [child] arguments must not be null.
25
+  ZefyrTheme({
26
+    Key key,
27
+    @required this.data,
28
+    @required Widget child,
29
+  })  : assert(data != null),
30
+        assert(child != null),
31
+        super(key: key, child: child);
32
+
33
+  @override
34
+  bool updateShouldNotify(ZefyrTheme oldWidget) {
35
+    return data != oldWidget.data;
36
+  }
37
+
38
+  /// The data from the closest [ZefyrTheme] instance that encloses the given
39
+  /// context.
40
+  ///
41
+  /// Returns `null` if there is no [ZefyrTheme] in the given build context
42
+  /// and [nullOk] is set to `true`. If [nullOk] is set to `false` (default)
43
+  /// then this method asserts.
44
+  static ZefyrThemeData of(BuildContext context, {bool nullOk = false}) {
45
+    final ZefyrTheme widget =
46
+        context.dependOnInheritedWidgetOfExactType<ZefyrTheme>();
47
+    if (widget == null && nullOk) return null;
48
+    assert(widget != null,
49
+        '$ZefyrTheme.of() called with a context that does not contain a ZefyrEditor.');
50
+    return widget.data;
51
+  }
52
+}
53
+
54
+/// Holds colors and typography styles for [ZefyrEditor].
55
+class ZefyrThemeData {
56
+  final TextStyle boldStyle;
57
+  final TextStyle italicStyle;
58
+  final TextStyle linkStyle;
59
+  final StyleTheme paragraphTheme;
60
+  final HeadingTheme headingTheme;
61
+  final BlockTheme blockTheme;
62
+  final Color selectionColor;
63
+  final Color cursorColor;
64
+
65
+  /// Size of indentation for blocks.
66
+  final double indentSize;
67
+  final ZefyrToolbarTheme toolbarTheme;
68
+
69
+  factory ZefyrThemeData.fallback(BuildContext context) {
70
+    final ThemeData themeData = Theme.of(context);
71
+    final defaultStyle = DefaultTextStyle.of(context);
72
+    final paragraphStyle = defaultStyle.style.copyWith(
73
+      fontSize: 16.0,
74
+      height: 1.3,
75
+    );
76
+    const padding = EdgeInsets.symmetric(vertical: 8.0);
77
+    final boldStyle = TextStyle(fontWeight: FontWeight.bold);
78
+    final italicStyle = TextStyle(fontStyle: FontStyle.italic);
79
+    final linkStyle = TextStyle(
80
+        color: themeData.accentColor, decoration: TextDecoration.underline);
81
+
82
+    return ZefyrThemeData(
83
+      boldStyle: boldStyle,
84
+      italicStyle: italicStyle,
85
+      linkStyle: linkStyle,
86
+      paragraphTheme: StyleTheme(textStyle: paragraphStyle, padding: padding),
87
+      headingTheme: HeadingTheme.fallback(context),
88
+      blockTheme: BlockTheme.fallback(context),
89
+      selectionColor: themeData.textSelectionColor,
90
+      cursorColor: themeData.cursorColor,
91
+      indentSize: 16.0,
92
+      toolbarTheme: ZefyrToolbarTheme.fallback(context),
93
+    );
94
+  }
95
+
96
+  const ZefyrThemeData({
97
+    this.boldStyle,
98
+    this.italicStyle,
99
+    this.linkStyle,
100
+    this.paragraphTheme,
101
+    this.headingTheme,
102
+    this.blockTheme,
103
+    this.selectionColor,
104
+    this.cursorColor,
105
+    this.indentSize,
106
+    this.toolbarTheme,
107
+  });
108
+
109
+  ZefyrThemeData copyWith({
110
+    TextStyle textStyle,
111
+    TextStyle boldStyle,
112
+    TextStyle italicStyle,
113
+    TextStyle linkStyle,
114
+    StyleTheme paragraphTheme,
115
+    HeadingTheme headingTheme,
116
+    BlockTheme blockTheme,
117
+    Color selectionColor,
118
+    Color cursorColor,
119
+    double indentSize,
120
+    ZefyrToolbarTheme toolbarTheme,
121
+  }) {
122
+    return ZefyrThemeData(
123
+      boldStyle: boldStyle ?? this.boldStyle,
124
+      italicStyle: italicStyle ?? this.italicStyle,
125
+      linkStyle: linkStyle ?? this.linkStyle,
126
+      paragraphTheme: paragraphTheme ?? this.paragraphTheme,
127
+      headingTheme: headingTheme ?? this.headingTheme,
128
+      blockTheme: blockTheme ?? this.blockTheme,
129
+      selectionColor: selectionColor ?? this.selectionColor,
130
+      cursorColor: cursorColor ?? this.cursorColor,
131
+      indentSize: indentSize ?? this.indentSize,
132
+      toolbarTheme: toolbarTheme ?? this.toolbarTheme,
133
+    );
134
+  }
135
+
136
+  ZefyrThemeData merge(ZefyrThemeData other) {
137
+    return copyWith(
138
+      boldStyle: other.boldStyle,
139
+      italicStyle: other.italicStyle,
140
+      linkStyle: other.linkStyle,
141
+      paragraphTheme: other.paragraphTheme,
142
+      headingTheme: other.headingTheme,
143
+      blockTheme: other.blockTheme,
144
+      selectionColor: other.selectionColor,
145
+      cursorColor: other.cursorColor,
146
+      indentSize: other.indentSize,
147
+      toolbarTheme: other.toolbarTheme,
148
+    );
149
+  }
150
+}
151
+
152
+/// Theme for heading-styled lines of text.
153
+class HeadingTheme {
154
+  /// Style theme for level 1 headings.
155
+  final StyleTheme level1;
156
+
157
+  /// Style theme for level 2 headings.
158
+  final StyleTheme level2;
159
+
160
+  /// Style theme for level 3 headings.
161
+  final StyleTheme level3;
162
+
163
+  HeadingTheme({
164
+    @required this.level1,
165
+    @required this.level2,
166
+    @required this.level3,
167
+  });
168
+
169
+  /// Creates fallback theme for headings.
170
+  factory HeadingTheme.fallback(BuildContext context) {
171
+    final defaultStyle = DefaultTextStyle.of(context);
172
+    return HeadingTheme(
173
+      level1: StyleTheme(
174
+        textStyle: defaultStyle.style.copyWith(
175
+          fontSize: 34.0,
176
+          color: defaultStyle.style.color.withOpacity(0.70),
177
+          height: 1.15,
178
+          fontWeight: FontWeight.w300,
179
+        ),
180
+        padding: EdgeInsets.only(top: 16.0, bottom: 0.0),
181
+      ),
182
+      level2: StyleTheme(
183
+        textStyle: TextStyle(
184
+          fontSize: 24.0,
185
+          color: defaultStyle.style.color.withOpacity(0.70),
186
+          height: 1.15,
187
+          fontWeight: FontWeight.normal,
188
+        ),
189
+        padding: EdgeInsets.only(bottom: 0.0, top: 8.0),
190
+      ),
191
+      level3: StyleTheme(
192
+        textStyle: TextStyle(
193
+          fontSize: 20.0,
194
+          color: defaultStyle.style.color.withOpacity(0.70),
195
+          height: 1.25,
196
+          fontWeight: FontWeight.w500,
197
+        ),
198
+        padding: EdgeInsets.only(bottom: 0.0, top: 8.0),
199
+      ),
200
+    );
201
+  }
202
+}
203
+
204
+/// Theme for a block of lines in a document.
205
+class BlockTheme {
206
+  /// Style theme for bullet lists.
207
+  final StyleTheme bulletList;
208
+
209
+  /// Style theme for number lists.
210
+  final StyleTheme numberList;
211
+
212
+  /// Style theme for code snippets.
213
+  final StyleTheme code;
214
+
215
+  /// Style theme for quotes.
216
+  final StyleTheme quote;
217
+
218
+  BlockTheme({
219
+    @required this.bulletList,
220
+    @required this.numberList,
221
+    @required this.quote,
222
+    @required this.code,
223
+  });
224
+
225
+  /// Creates fallback theme for blocks.
226
+  factory BlockTheme.fallback(BuildContext context) {
227
+    final themeData = Theme.of(context);
228
+    final defaultTextStyle = DefaultTextStyle.of(context);
229
+    final padding = const EdgeInsets.symmetric(vertical: 8.0);
230
+    String fontFamily;
231
+    switch (themeData.platform) {
232
+      case TargetPlatform.iOS:
233
+        fontFamily = 'Menlo';
234
+        break;
235
+      case TargetPlatform.android:
236
+      case TargetPlatform.fuchsia:
237
+        fontFamily = 'Roboto Mono';
238
+        break;
239
+    }
240
+
241
+    return BlockTheme(
242
+      bulletList: StyleTheme(padding: padding),
243
+      numberList: StyleTheme(padding: padding),
244
+      quote: StyleTheme(
245
+        textStyle: TextStyle(
246
+          color: defaultTextStyle.style.color.withOpacity(0.6),
247
+        ),
248
+        padding: padding,
249
+      ),
250
+      code: StyleTheme(
251
+        textStyle: TextStyle(
252
+          color: defaultTextStyle.style.color.withOpacity(0.8),
253
+          fontFamily: fontFamily,
254
+          fontSize: 14.0,
255
+          height: 1.25,
256
+        ),
257
+        padding: padding,
258
+      ),
259
+    );
260
+  }
261
+}
262
+
263
+/// Theme for a specific attribute style.
264
+///
265
+/// Used in [HeadingTheme] and [BlockTheme], as well as in
266
+/// [ZefyrThemeData.paragraphTheme].
267
+class StyleTheme {
268
+  /// Text style of this theme.
269
+  final TextStyle textStyle;
270
+
271
+  /// Padding to apply around lines of text.
272
+  final EdgeInsets padding;
273
+
274
+  /// Creates a new [StyleTheme].
275
+  StyleTheme({
276
+    this.textStyle,
277
+    this.padding,
278
+  });
279
+}
280
+
281
+/// Defines styles and colors for [ZefyrToolbar].
282
+class ZefyrToolbarTheme {
283
+  /// The background color of toolbar.
284
+  final Color color;
285
+
286
+  /// Color of buttons in toggled state.
287
+  final Color toggleColor;
288
+
289
+  /// Color of button icons.
290
+  final Color iconColor;
291
+
292
+  /// Color of button icons in disabled state.
293
+  final Color disabledIconColor;
294
+
295
+  /// Creates fallback theme for editor toolbars.
296
+  factory ZefyrToolbarTheme.fallback(BuildContext context) {
297
+    final theme = Theme.of(context);
298
+    return ZefyrToolbarTheme._(
299
+      color: theme.primaryColorBrightness == Brightness.light
300
+          ? Colors.grey.shade300
301
+          : Colors.grey.shade800,
302
+      toggleColor: theme.primaryColorBrightness == Brightness.light
303
+          ? Colors.grey.shade400
304
+          : Colors.grey.shade900,
305
+      iconColor: theme.primaryIconTheme.color,
306
+      disabledIconColor: theme.disabledColor,
307
+    );
308
+  }
309
+
310
+  ZefyrToolbarTheme._({
311
+    @required this.color,
312
+    @required this.toggleColor,
313
+    @required this.iconColor,
314
+    @required this.disabledIconColor,
315
+  });
316
+
317
+  ZefyrToolbarTheme copyWith({
318
+    Color color,
319
+    Color toggleColor,
320
+    Color iconColor,
321
+    Color disabledIconColor,
322
+  }) {
323
+    return ZefyrToolbarTheme._(
324
+      color: color ?? this.color,
325
+      toggleColor: toggleColor ?? this.toggleColor,
326
+      iconColor: iconColor ?? this.iconColor,
327
+      disabledIconColor: disabledIconColor ?? this.disabledIconColor,
328
+    );
329
+  }
330
+}

+ 1
- 1
packages/zefyr/lib/src/widgets/buttons.dart Vedi File

@@ -93,7 +93,7 @@ class ZefyrButton extends StatelessWidget {
93 93
     }
94 94
   }
95 95
 
96
-  Color _getColor(ZefyrScope editor, ZefyrToolbarTheme theme) {
96
+  Color _getColor(ZefyrScope editor, ToolbarTheme theme) {
97 97
     if (isAttributeAction) {
98 98
       final attribute = kZefyrToolbarAttributeActions[action];
99 99
       final isToggled = (attribute is NotusAttribute)

+ 10
- 6
packages/zefyr/lib/src/widgets/code.dart Vedi File

@@ -16,19 +16,23 @@ class ZefyrCode extends StatelessWidget {
16 16
 
17 17
   @override
18 18
   Widget build(BuildContext context) {
19
-    final theme = ZefyrTheme.of(context);
19
+    final theme = Theme.of(context);
20
+    final zefyrTheme = ZefyrTheme.of(context);
20 21
 
21 22
     List<Widget> items = [];
22 23
     for (var line in node.children) {
23
-      items.add(_buildLine(line, theme.blockTheme.code.textStyle));
24
+      items.add(_buildLine(line, zefyrTheme.attributeTheme.code.textStyle));
24 25
     }
25 26
 
27
+    // TODO: move background color and decoration to BlockTheme
28
+    final color = theme.primaryColorBrightness == Brightness.light
29
+        ? Colors.grey.shade200
30
+        : Colors.grey.shade800;
26 31
     return Padding(
27
-      padding: theme.blockTheme.code.padding,
32
+      padding: zefyrTheme.attributeTheme.code.padding,
28 33
       child: Container(
29
-        // TODO: make decorations configurable
30 34
         decoration: BoxDecoration(
31
-          color: Colors.blueGrey.shade50,
35
+          color: color,
32 36
           borderRadius: BorderRadius.circular(3.0),
33 37
         ),
34 38
         padding: const EdgeInsets.all(16.0),
@@ -42,6 +46,6 @@ class ZefyrCode extends StatelessWidget {
42 46
 
43 47
   Widget _buildLine(Node node, TextStyle style) {
44 48
     LineNode line = node;
45
-    return RawZefyrLine(node: line, style: style);
49
+    return ZefyrLine(node: line, style: style);
46 50
   }
47 51
 }

+ 27
- 19
packages/zefyr/lib/src/widgets/common.dart Vedi File

@@ -1,6 +1,8 @@
1 1
 // Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2 2
 // for details. All rights reserved. Use of this source code is governed by a
3 3
 // BSD-style license that can be found in the LICENSE file.
4
+import 'package:flutter/cupertino.dart';
5
+import 'package:flutter/material.dart';
4 6
 import 'package:flutter/rendering.dart';
5 7
 import 'package:flutter/widgets.dart';
6 8
 import 'package:notus/notus.dart';
@@ -12,17 +14,11 @@ import 'rich_text.dart';
12 14
 import 'scope.dart';
13 15
 import 'theme.dart';
14 16
 
15
-/// Raw widget representing a single line of rich text document in Zefyr editor.
16
-///
17
-/// See [ZefyrParagraph] and [ZefyrHeading] which wrap this widget and
18
-/// integrate it with current [ZefyrTheme].
19
-class RawZefyrLine extends StatefulWidget {
20
-  const RawZefyrLine({
21
-    Key key,
22
-    @required this.node,
23
-    this.style,
24
-    this.padding,
25
-  }) : super(key: key);
17
+/// Represents single line of rich text document in Zefyr editor.
18
+class ZefyrLine extends StatefulWidget {
19
+  const ZefyrLine({Key key, @required this.node, this.style, this.padding})
20
+      : assert(node != null),
21
+        super(key: key);
26 22
 
27 23
   /// Line in the document represented by this widget.
28 24
   final LineNode node;
@@ -35,10 +31,10 @@ class RawZefyrLine extends StatefulWidget {
35 31
   final EdgeInsets padding;
36 32
 
37 33
   @override
38
-  _RawZefyrLineState createState() => _RawZefyrLineState();
34
+  _ZefyrLineState createState() => _ZefyrLineState();
39 35
 }
40 36
 
41
-class _RawZefyrLineState extends State<RawZefyrLine> {
37
+class _ZefyrLineState extends State<ZefyrLine> {
42 38
   final LayerLink _link = LayerLink();
43 39
 
44 40
   @override
@@ -47,7 +43,7 @@ class _RawZefyrLineState extends State<RawZefyrLine> {
47 43
     if (scope.isEditable) {
48 44
       ensureVisible(context, scope);
49 45
     }
50
-    final theme = ZefyrTheme.of(context);
46
+    final theme = Theme.of(context);
51 47
 
52 48
     Widget content;
53 49
     if (widget.node.hasEmbed) {
@@ -61,6 +57,18 @@ class _RawZefyrLineState extends State<RawZefyrLine> {
61 57
     }
62 58
 
63 59
     if (scope.isEditable) {
60
+      Color cursorColor;
61
+      switch (theme.platform) {
62
+        case TargetPlatform.iOS:
63
+          cursorColor ??= CupertinoTheme.of(context).primaryColor;
64
+          break;
65
+
66
+        case TargetPlatform.android:
67
+        case TargetPlatform.fuchsia:
68
+          cursorColor = theme.cursorColor;
69
+          break;
70
+      }
71
+
64 72
       content = EditableBox(
65 73
         child: content,
66 74
         node: widget.node,
@@ -68,8 +76,8 @@ class _RawZefyrLineState extends State<RawZefyrLine> {
68 76
         renderContext: scope.renderContext,
69 77
         showCursor: scope.showCursor,
70 78
         selection: scope.selection,
71
-        selectionColor: theme.selectionColor,
72
-        cursorColor: theme.cursorColor,
79
+        selectionColor: theme.textSelectionColor,
80
+        cursorColor: cursorColor,
73 81
       );
74 82
       content = CompositedTransformTarget(link: _link, child: content);
75 83
     }
@@ -129,13 +137,13 @@ class _RawZefyrLineState extends State<RawZefyrLine> {
129 137
   TextStyle _getTextStyle(NotusStyle style, ZefyrThemeData theme) {
130 138
     TextStyle result = TextStyle();
131 139
     if (style.containsSame(NotusAttribute.bold)) {
132
-      result = result.merge(theme.boldStyle);
140
+      result = result.merge(theme.attributeTheme.bold);
133 141
     }
134 142
     if (style.containsSame(NotusAttribute.italic)) {
135
-      result = result.merge(theme.italicStyle);
143
+      result = result.merge(theme.attributeTheme.italic);
136 144
     }
137 145
     if (style.contains(NotusAttribute.link)) {
138
-      result = result.merge(theme.linkStyle);
146
+      result = result.merge(theme.attributeTheme.link);
139 147
     }
140 148
     return result;
141 149
   }

+ 9
- 6
packages/zefyr/lib/src/widgets/editable_box.dart Vedi File

@@ -72,7 +72,7 @@ class RenderEditableProxyBox extends RenderBox
72 72
     @required TextSelection selection,
73 73
     @required Color selectionColor,
74 74
     @required Color cursorColor,
75
-  })  : _node = node,
75
+  })  : node = node,
76 76
         _layerLink = layerLink,
77 77
         _renderContext = renderContext,
78 78
         _showCursor = showCursor,
@@ -94,11 +94,7 @@ class RenderEditableProxyBox extends RenderBox
94 94
 
95 95
   bool _isDirty = true;
96 96
 
97
-  ContainerNode get node => _node;
98
-  ContainerNode _node;
99
-  set node(ContainerNode value) {
100
-    _node = value;
101
-  }
97
+  ContainerNode node;
102 98
 
103 99
   LayerLink get layerLink => _layerLink;
104 100
   LayerLink _layerLink;
@@ -153,6 +149,13 @@ class RenderEditableProxyBox extends RenderBox
153 149
   /// Returns `true` if current selection is collapsed and located
154 150
   /// within this paragraph.
155 151
   bool get containsCaret {
152
+    if (!node.mounted) {
153
+      // It is possible that a document node gets unmounted before widget tree
154
+      // is updated, in which case this function fails when triggered by
155
+      // _showCursor notification calling markNeedsCursorPaint.
156
+      // TODO: react to document node's mounted state.
157
+      return false;
158
+    }
156 159
     if (!_selection.isCollapsed) return false;
157 160
 
158 161
     final int start = node.documentOffset;

+ 1
- 1
packages/zefyr/lib/src/widgets/editable_text.dart Vedi File

@@ -239,7 +239,7 @@ class _ZefyrEditableTextState extends State<ZefyrEditableText>
239 239
   Widget _defaultChildBuilder(BuildContext context, Node node) {
240 240
     if (node is LineNode) {
241 241
       if (node.hasEmbed) {
242
-        return RawZefyrLine(node: node);
242
+        return ZefyrLine(node: node);
243 243
       } else if (node.style.contains(NotusAttribute.heading)) {
244 244
         return ZefyrHeading(node: node);
245 245
       }

+ 3
- 2
packages/zefyr/lib/src/widgets/field.dart Vedi File

@@ -47,6 +47,7 @@ class ZefyrField extends StatefulWidget {
47 47
 }
48 48
 
49 49
 class _ZefyrFieldState extends State<ZefyrField> {
50
+  ZefyrMode get _effectiveMode => widget.mode ?? ZefyrMode.edit;
50 51
   @override
51 52
   Widget build(BuildContext context) {
52 53
     Widget child = ZefyrEditor(
@@ -54,7 +55,7 @@ class _ZefyrFieldState extends State<ZefyrField> {
54 55
       controller: widget.controller,
55 56
       focusNode: widget.focusNode,
56 57
       autofocus: widget.autofocus,
57
-      mode: widget.mode ?? ZefyrMode.edit,
58
+      mode: _effectiveMode,
58 59
       toolbarDelegate: widget.toolbarDelegate,
59 60
       imageDelegate: widget.imageDelegate,
60 61
       physics: widget.physics,
@@ -88,7 +89,7 @@ class _ZefyrFieldState extends State<ZefyrField> {
88 89
         (widget.decoration ?? const InputDecoration())
89 90
             .applyDefaults(Theme.of(context).inputDecorationTheme)
90 91
             .copyWith(
91
-              enabled: widget.mode == ZefyrMode.edit,
92
+              enabled: _effectiveMode == ZefyrMode.edit,
92 93
             );
93 94
 
94 95
     return effectiveDecoration;

+ 14
- 16
packages/zefyr/lib/src/widgets/image.dart Vedi File

@@ -9,6 +9,7 @@ import 'package:flutter/rendering.dart';
9 9
 import 'package:flutter/widgets.dart';
10 10
 import 'package:meta/meta.dart';
11 11
 import 'package:notus/notus.dart';
12
+import 'package:zefyr/zefyr.dart';
12 13
 
13 14
 import 'editable_box.dart';
14 15
 
@@ -58,9 +59,13 @@ class _ZefyrImageState extends State<ZefyrImage> {
58 59
 
59 60
   @override
60 61
   Widget build(BuildContext context) {
62
+    final theme = ZefyrTheme.of(context);
61 63
     final image = widget.delegate.buildImage(context, imageSource);
62 64
     return _EditableImage(
63
-      child: image,
65
+      child: Padding(
66
+        padding: theme.defaultLineTheme.padding,
67
+        child: image,
68
+      ),
64 69
       node: widget.node,
65 70
     );
66 71
   }
@@ -88,25 +93,19 @@ class _EditableImage extends SingleChildRenderObjectWidget {
88 93
 class RenderEditableImage extends RenderBox
89 94
     with RenderObjectWithChildMixin<RenderBox>, RenderProxyBoxMixin<RenderBox>
90 95
     implements RenderEditableBox {
91
-  static const kPaddingBottom = 24.0;
92
-
93 96
   RenderEditableImage({
94 97
     RenderImage child,
95 98
     @required EmbedNode node,
96
-  }) : _node = node {
99
+  }) : node = node {
97 100
     this.child = child;
98 101
   }
99 102
 
100 103
   @override
101
-  EmbedNode get node => _node;
102
-  EmbedNode _node;
103
-  set node(EmbedNode value) {
104
-    _node = value;
105
-  }
104
+  EmbedNode node;
106 105
 
107 106
   // TODO: Customize caret height offset instead of adjusting here by 2px.
108 107
   @override
109
-  double get preferredLineHeight => size.height - kPaddingBottom + 2.0;
108
+  double get preferredLineHeight => size.height + 2.0;
110 109
 
111 110
   @override
112 111
   SelectionOrder get selectionOrder => SelectionOrder.foreground;
@@ -129,8 +128,7 @@ class RenderEditableImage extends RenderBox
129 128
     if (local.isCollapsed) {
130 129
       final dx = local.extentOffset == 0 ? _childOffset.dx : size.width;
131 130
       return [
132
-        ui.TextBox.fromLTRBD(
133
-            dx, 0.0, dx, size.height - kPaddingBottom, TextDirection.ltr),
131
+        ui.TextBox.fromLTRBD(dx, 0.0, dx, size.height, TextDirection.ltr),
134 132
       ];
135 133
     }
136 134
 
@@ -145,7 +143,7 @@ class RenderEditableImage extends RenderBox
145 143
 
146 144
   @override
147 145
   TextPosition getPositionForOffset(Offset offset) {
148
-    int position = _node.documentOffset;
146
+    int position = node.documentOffset;
149 147
 
150 148
     if (offset.dx > size.width / 2) {
151 149
       position++;
@@ -155,7 +153,7 @@ class RenderEditableImage extends RenderBox
155 153
 
156 154
   @override
157 155
   TextRange getWordBoundary(TextPosition position) {
158
-    final start = _node.documentOffset;
156
+    final start = node.documentOffset;
159 157
     return TextRange(start: start, end: start + 1);
160 158
   }
161 159
 
@@ -203,7 +201,7 @@ class RenderEditableImage extends RenderBox
203 201
 
204 202
   Offset get _childOffset {
205 203
     final dx = (size.width - _lastChildSize.width) / 2 + kHorizontalPadding;
206
-    final dy = (size.height - _lastChildSize.height - kPaddingBottom) / 2;
204
+    final dy = (size.height - _lastChildSize.height) / 2;
207 205
     return Offset(dx, dy);
208 206
   }
209 207
 
@@ -226,7 +224,7 @@ class RenderEditableImage extends RenderBox
226 224
       );
227 225
       child.layout(childConstraints, parentUsesSize: true);
228 226
       _lastChildSize = child.size;
229
-      size = Size(constraints.maxWidth, _lastChildSize.height + kPaddingBottom);
227
+      size = Size(constraints.maxWidth, _lastChildSize.height);
230 228
     } else {
231 229
       performResize();
232 230
     }

+ 13
- 5
packages/zefyr/lib/src/widgets/list.dart Vedi File

@@ -27,9 +27,9 @@ class ZefyrList extends StatelessWidget {
27 27
     final isNumberList =
28 28
         node.style.get(NotusAttribute.block) == NotusAttribute.block.numberList;
29 29
     EdgeInsets padding = isNumberList
30
-        ? theme.blockTheme.numberList.padding
31
-        : theme.blockTheme.bulletList.padding;
32
-    padding = padding.copyWith(left: theme.indentSize);
30
+        ? theme.attributeTheme.numberList.padding
31
+        : theme.attributeTheme.bulletList.padding;
32
+    padding = padding.copyWith(left: theme.indentWidth);
33 33
 
34 34
     return Padding(
35 35
       padding: padding,
@@ -55,6 +55,9 @@ class ZefyrListItem extends StatelessWidget {
55 55
     final BlockNode block = node.parent;
56 56
     final style = block.style.get(NotusAttribute.block);
57 57
     final theme = ZefyrTheme.of(context);
58
+    final blockTheme = (style == NotusAttribute.block.bulletList)
59
+        ? theme.attributeTheme.bulletList
60
+        : theme.attributeTheme.numberList;
58 61
     final bulletText =
59 62
         (style == NotusAttribute.block.bulletList) ? '•' : '$index.';
60 63
 
@@ -68,8 +71,13 @@ class ZefyrListItem extends StatelessWidget {
68 71
       padding = headingTheme.padding;
69 72
       content = ZefyrHeading(node: node);
70 73
     } else {
71
-      textStyle = theme.paragraphTheme.textStyle;
72
-      content = RawZefyrLine(node: node, style: textStyle);
74
+      textStyle = theme.defaultLineTheme.textStyle;
75
+      content = ZefyrLine(
76
+        node: node,
77
+        style: textStyle,
78
+        padding: blockTheme.linePadding,
79
+      );
80
+      padding = blockTheme.linePadding;
73 81
     }
74 82
 
75 83
     Widget bullet =

+ 8
- 8
packages/zefyr/lib/src/widgets/paragraph.dart Vedi File

@@ -18,14 +18,14 @@ class ZefyrParagraph extends StatelessWidget {
18 18
   @override
19 19
   Widget build(BuildContext context) {
20 20
     final theme = ZefyrTheme.of(context);
21
-    TextStyle style = theme.paragraphTheme.textStyle;
21
+    TextStyle style = theme.defaultLineTheme.textStyle;
22 22
     if (blockStyle != null) {
23 23
       style = style.merge(blockStyle);
24 24
     }
25
-    return RawZefyrLine(
25
+    return ZefyrLine(
26 26
       node: node,
27 27
       style: style,
28
-      padding: theme.paragraphTheme.padding,
28
+      padding: theme.defaultLineTheme.padding,
29 29
     );
30 30
   }
31 31
 }
@@ -46,22 +46,22 @@ class ZefyrHeading extends StatelessWidget {
46 46
     if (blockStyle != null) {
47 47
       style = style.merge(blockStyle);
48 48
     }
49
-    return RawZefyrLine(
49
+    return ZefyrLine(
50 50
       node: node,
51 51
       style: style,
52 52
       padding: theme.padding,
53 53
     );
54 54
   }
55 55
 
56
-  static StyleTheme themeOf(LineNode node, BuildContext context) {
56
+  static LineTheme themeOf(LineNode node, BuildContext context) {
57 57
     final theme = ZefyrTheme.of(context);
58 58
     final style = node.style.get(NotusAttribute.heading);
59 59
     if (style == NotusAttribute.heading.level1) {
60
-      return theme.headingTheme.level1;
60
+      return theme.attributeTheme.heading1;
61 61
     } else if (style == NotusAttribute.heading.level2) {
62
-      return theme.headingTheme.level2;
62
+      return theme.attributeTheme.heading2;
63 63
     } else if (style == NotusAttribute.heading.level3) {
64
-      return theme.headingTheme.level3;
64
+      return theme.attributeTheme.heading3;
65 65
     }
66 66
     throw UnimplementedError('Unsupported heading style $style');
67 67
   }

+ 3
- 3
packages/zefyr/lib/src/widgets/quote.dart Vedi File

@@ -16,14 +16,14 @@ class ZefyrQuote extends StatelessWidget {
16 16
   @override
17 17
   Widget build(BuildContext context) {
18 18
     final theme = ZefyrTheme.of(context);
19
-    final style = theme.blockTheme.quote.textStyle;
19
+    final style = theme.attributeTheme.quote.textStyle;
20 20
     List<Widget> items = [];
21 21
     for (var line in node.children) {
22
-      items.add(_buildLine(line, style, theme.indentSize));
22
+      items.add(_buildLine(line, style, theme.indentWidth));
23 23
     }
24 24
 
25 25
     return Padding(
26
-      padding: theme.blockTheme.quote.padding,
26
+      padding: theme.attributeTheme.quote.padding,
27 27
       child: Column(
28 28
         crossAxisAlignment: CrossAxisAlignment.stretch,
29 29
         children: items,

+ 8
- 12
packages/zefyr/lib/src/widgets/rich_text.dart Vedi File

@@ -50,7 +50,7 @@ class RenderZefyrParagraph extends RenderParagraph
50 50
     TextOverflow overflow = TextOverflow.clip,
51 51
     double textScaleFactor = 1.0,
52 52
     int maxLines,
53
-  })  : _node = node,
53
+  })  : node = node,
54 54
         _prototypePainter = TextPainter(
55 55
           text: TextSpan(text: '.', style: text.style),
56 56
           textAlign: textAlign,
@@ -67,11 +67,7 @@ class RenderZefyrParagraph extends RenderParagraph
67 67
           maxLines: maxLines,
68 68
         );
69 69
 
70
-  LineNode get node => _node;
71
-  LineNode _node;
72
-  set node(LineNode value) {
73
-    _node = value;
74
-  }
70
+  LineNode node;
75 71
 
76 72
   @override
77 73
   double get preferredLineHeight => _prototypePainter.height;
@@ -95,7 +91,7 @@ class RenderZefyrParagraph extends RenderParagraph
95 91
   TextPosition getPositionForOffset(Offset offset) {
96 92
     final position = super.getPositionForOffset(offset);
97 93
     return TextPosition(
98
-      offset: _node.documentOffset + position.offset,
94
+      offset: node.documentOffset + position.offset,
99 95
       affinity: position.affinity,
100 96
     );
101 97
   }
@@ -103,20 +99,20 @@ class RenderZefyrParagraph extends RenderParagraph
103 99
   @override
104 100
   TextRange getWordBoundary(TextPosition position) {
105 101
     final localPosition = TextPosition(
106
-      offset: position.offset - _node.documentOffset,
102
+      offset: position.offset - node.documentOffset,
107 103
       affinity: position.affinity,
108 104
     );
109 105
     final localRange = super.getWordBoundary(localPosition);
110 106
     return TextRange(
111
-      start: _node.documentOffset + localRange.start,
112
-      end: _node.documentOffset + localRange.end,
107
+      start: node.documentOffset + localRange.start,
108
+      end: node.documentOffset + localRange.end,
113 109
     );
114 110
   }
115 111
 
116 112
   @override
117 113
   Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
118 114
     final localPosition = TextPosition(
119
-      offset: position.offset - _node.documentOffset,
115
+      offset: position.offset - node.documentOffset,
120 116
       affinity: position.affinity,
121 117
     );
122 118
     return super.getOffsetForCaret(localPosition, caretPrototype);
@@ -169,7 +165,7 @@ class RenderZefyrParagraph extends RenderParagraph
169 165
     }
170 166
     if (isExtentShifted) {
171 167
       final box = result.last;
172
-      result.removeLast;
168
+      result.removeLast();
173 169
       result.add(ui.TextBox.fromLTRBD(
174 170
           box.left, box.top, box.left, box.bottom, box.direction));
175 171
     }

+ 1
- 1
packages/zefyr/lib/src/widgets/selection.dart Vedi File

@@ -381,7 +381,7 @@ class _SelectionHandleDriverState extends State<SelectionHandleDriver>
381 381
       // TODO: For some reason sometimes we get updates when render boxes
382 382
       //      are in process of rebuilding so we don't have access to them here.
383 383
       //      As a workaround we just return empty container. There is usually
384
-      //      another rebuild right after which "fixes" the view.
384
+      //      another rebuild right after this one which "fixes" the view.
385 385
       //      Example: when toolbar button is toggled changing style of current
386 386
       //      selection.
387 387
       return Container();

+ 433
- 190
packages/zefyr/lib/src/widgets/theme.dart Vedi File

@@ -1,21 +1,11 @@
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 1
 import 'package:flutter/material.dart';
6
-import 'package:flutter/widgets.dart';
7
-import 'package:meta/meta.dart';
8 2
 
9 3
 /// Applies a Zefyr editor theme to descendant widgets.
10 4
 ///
11
-/// Describes colors and typographic styles for an editor.
5
+/// Describes colors and typographic styles.
12 6
 ///
13 7
 /// Descendant widgets obtain the current theme's [ZefyrThemeData] object using
14 8
 /// [ZefyrTheme.of].
15
-///
16
-/// See also:
17
-///
18
-///   * [ZefyrThemeData], which describes actual configuration of a theme.
19 9
 class ZefyrTheme extends InheritedWidget {
20 10
   final ZefyrThemeData data;
21 11
 
@@ -51,233 +41,454 @@ class ZefyrTheme extends InheritedWidget {
51 41
   }
52 42
 }
53 43
 
54
-/// Holds colors and typography styles for [ZefyrEditor].
44
+/// Holds colors and typography values for a Zefyr design theme.
45
+///
46
+/// To obtain the current theme, use [ZefyrTheme.of].
47
+@immutable
55 48
 class ZefyrThemeData {
56
-  final TextStyle boldStyle;
57
-  final TextStyle italicStyle;
58
-  final TextStyle linkStyle;
59
-  final StyleTheme paragraphTheme;
60
-  final HeadingTheme headingTheme;
61
-  final BlockTheme blockTheme;
62
-  final Color selectionColor;
63
-  final Color cursorColor;
64
-
65
-  /// Size of indentation for blocks.
66
-  final double indentSize;
67
-  final ZefyrToolbarTheme toolbarTheme;
49
+  /// Default theme used for document lines in Zefyr editor.
50
+  ///
51
+  /// Defines text style and spacing for regular paragraphs of text with
52
+  /// no style attributes applied.
53
+  final LineTheme defaultLineTheme;
68 54
 
55
+  /// The text styles, padding and decorations used to render text with
56
+  /// different style attributes.
57
+  final AttributeTheme attributeTheme;
58
+
59
+  /// The width of indentation used for blocks (lists, quotes, code).
60
+  final double indentWidth;
61
+
62
+  /// The colors used to render editor toolbar.
63
+  final ToolbarTheme toolbarTheme;
64
+
65
+  /// Creates a [ZefyrThemeData] given a set of exact values.
66
+  const ZefyrThemeData({
67
+    this.defaultLineTheme,
68
+    this.attributeTheme,
69
+    this.indentWidth,
70
+    this.toolbarTheme,
71
+  });
72
+
73
+  /// The default editor theme.
69 74
   factory ZefyrThemeData.fallback(BuildContext context) {
70
-    final ThemeData themeData = Theme.of(context);
71 75
     final defaultStyle = DefaultTextStyle.of(context);
72
-    final paragraphStyle = defaultStyle.style.copyWith(
73
-      fontSize: 16.0,
74
-      height: 1.25,
75
-      fontWeight: FontWeight.normal,
76
-      color: Colors.grey.shade800,
76
+    final defaultLineTheme = LineTheme(
77
+      textStyle: defaultStyle.style.copyWith(
78
+        fontSize: 16.0,
79
+        height: 1.3,
80
+      ),
81
+      padding: EdgeInsets.symmetric(vertical: 8.0),
77 82
     );
78
-    final padding = const EdgeInsets.only(bottom: 16.0);
79
-    final boldStyle = TextStyle(fontWeight: FontWeight.bold);
80
-    final italicStyle = TextStyle(fontStyle: FontStyle.italic);
81
-    final linkStyle =
82
-        TextStyle(color: Colors.blue, decoration: TextDecoration.underline);
83
-
84 83
     return ZefyrThemeData(
85
-      boldStyle: boldStyle,
86
-      italicStyle: italicStyle,
87
-      linkStyle: linkStyle,
88
-      paragraphTheme: StyleTheme(textStyle: paragraphStyle, padding: padding),
89
-      headingTheme: HeadingTheme.fallback(),
90
-      blockTheme: BlockTheme.fallback(themeData),
91
-      selectionColor: Colors.lightBlueAccent.shade100,
92
-      cursorColor: Colors.black,
93
-      indentSize: 16.0,
94
-      toolbarTheme: ZefyrToolbarTheme.fallback(context),
84
+      defaultLineTheme: defaultLineTheme,
85
+      attributeTheme: AttributeTheme.fallback(context, defaultLineTheme),
86
+      indentWidth: 16.0,
87
+      toolbarTheme: ToolbarTheme.fallback(context),
95 88
     );
96 89
   }
97 90
 
98
-  const ZefyrThemeData({
99
-    this.boldStyle,
100
-    this.italicStyle,
101
-    this.linkStyle,
102
-    this.paragraphTheme,
103
-    this.headingTheme,
104
-    this.blockTheme,
105
-    this.selectionColor,
106
-    this.cursorColor,
107
-    this.indentSize,
108
-    this.toolbarTheme,
109
-  });
110
-
91
+  /// Creates a copy of this theme but with the given fields replaced with
92
+  /// the new values.
111 93
   ZefyrThemeData copyWith({
112
-    TextStyle textStyle,
113
-    TextStyle boldStyle,
114
-    TextStyle italicStyle,
115
-    TextStyle linkStyle,
116
-    StyleTheme paragraphTheme,
117
-    HeadingTheme headingTheme,
118
-    BlockTheme blockTheme,
119
-    Color selectionColor,
120
-    Color cursorColor,
121
-    double indentSize,
122
-    ZefyrToolbarTheme toolbarTheme,
94
+    LineTheme defaultLineTheme,
95
+    AttributeTheme attributeTheme,
96
+    double indentWidth,
97
+    ToolbarTheme toolbarTheme,
123 98
   }) {
124 99
     return ZefyrThemeData(
125
-      boldStyle: boldStyle ?? this.boldStyle,
126
-      italicStyle: italicStyle ?? this.italicStyle,
127
-      linkStyle: linkStyle ?? this.linkStyle,
128
-      paragraphTheme: paragraphTheme ?? this.paragraphTheme,
129
-      headingTheme: headingTheme ?? this.headingTheme,
130
-      blockTheme: blockTheme ?? this.blockTheme,
131
-      selectionColor: selectionColor ?? this.selectionColor,
132
-      cursorColor: cursorColor ?? this.cursorColor,
133
-      indentSize: indentSize ?? this.indentSize,
100
+      defaultLineTheme: defaultLineTheme ?? this.defaultLineTheme,
101
+      attributeTheme: attributeTheme ?? this.attributeTheme,
102
+      indentWidth: indentWidth ?? this.indentWidth,
134 103
       toolbarTheme: toolbarTheme ?? this.toolbarTheme,
135 104
     );
136 105
   }
137 106
 
107
+  /// Creates a new [ZefyrThemeData] where each property from this object has
108
+  /// been merged with the matching text style from the `other` object.
138 109
   ZefyrThemeData merge(ZefyrThemeData other) {
110
+    if (other == null) return this;
139 111
     return copyWith(
140
-      boldStyle: other.boldStyle,
141
-      italicStyle: other.italicStyle,
142
-      linkStyle: other.linkStyle,
143
-      paragraphTheme: other.paragraphTheme,
144
-      headingTheme: other.headingTheme,
145
-      blockTheme: other.blockTheme,
146
-      selectionColor: other.selectionColor,
147
-      cursorColor: other.cursorColor,
148
-      indentSize: other.indentSize,
149
-      toolbarTheme: other.toolbarTheme,
112
+      defaultLineTheme: defaultLineTheme?.merge(other.defaultLineTheme) ??
113
+          other.defaultLineTheme,
114
+      attributeTheme:
115
+          attributeTheme?.merge(other.attributeTheme) ?? other.attributeTheme,
116
+      indentWidth: other.indentWidth ?? indentWidth,
117
+      toolbarTheme:
118
+          toolbarTheme?.merge(other.toolbarTheme) ?? other.toolbarTheme,
150 119
     );
151 120
   }
121
+
122
+  @override
123
+  bool operator ==(other) {
124
+    if (other.runtimeType != runtimeType) return false;
125
+    final ZefyrThemeData otherData = other;
126
+    return (otherData.defaultLineTheme == defaultLineTheme) &&
127
+        (otherData.attributeTheme == attributeTheme) &&
128
+        (otherData.indentWidth == indentWidth) &&
129
+        (otherData.toolbarTheme == toolbarTheme);
130
+  }
131
+
132
+  @override
133
+  int get hashCode {
134
+    return hashList([
135
+      defaultLineTheme,
136
+      attributeTheme,
137
+      indentWidth,
138
+      toolbarTheme,
139
+    ]);
140
+  }
152 141
 }
153 142
 
154
-/// Theme for heading-styled lines of text.
155
-class HeadingTheme {
156
-  /// Style theme for level 1 headings.
157
-  final StyleTheme level1;
143
+/// Holds typography values for a document line in Zefyr editor.
144
+///
145
+/// Applicable for regular paragraphs, headings and lines within blocks
146
+/// (lists, quotes). Blocks may override some of these values using [BlockTheme].
147
+@immutable
148
+class LineTheme {
149
+  /// Default text style for a document line.
150
+  final TextStyle textStyle;
158 151
 
159
-  /// Style theme for level 2 headings.
160
-  final StyleTheme level2;
152
+  /// Additional space around a document line.
153
+  final EdgeInsets padding;
161 154
 
162
-  /// Style theme for level 3 headings.
163
-  final StyleTheme level3;
155
+  /// Creates a [LineTheme] given a set of exact values.
156
+  LineTheme({this.textStyle, this.padding})
157
+      : assert(textStyle != null),
158
+        assert(padding != null);
159
+
160
+  /// Creates a copy of this theme but with the given fields replaced with
161
+  /// the new values.
162
+  LineTheme copyWith({TextStyle textStyle, EdgeInsets padding}) {
163
+    return LineTheme(
164
+      textStyle: textStyle ?? this.textStyle,
165
+      padding: padding ?? this.padding,
166
+    );
167
+  }
164 168
 
165
-  HeadingTheme({
166
-    @required this.level1,
167
-    @required this.level2,
168
-    @required this.level3,
169
+  /// Creates a new [LineTheme] where each property from this object has
170
+  /// been merged with the matching property from the `other` object.
171
+  ///
172
+  /// Text style is merged using [TextStyle.merge] when this and other
173
+  /// theme have this value set.
174
+  ///
175
+  /// If padding property is set in other then it replaces value of this
176
+  /// theme.
177
+  LineTheme merge(LineTheme other) {
178
+    if (other == null) return this;
179
+    return copyWith(
180
+      textStyle: textStyle?.merge(other.textStyle) ?? other.textStyle,
181
+      padding: other.padding ?? padding,
182
+    );
183
+  }
184
+
185
+  @override
186
+  bool operator ==(other) {
187
+    if (other.runtimeType != runtimeType) return false;
188
+    final LineTheme otherTheme = other;
189
+    return (otherTheme.textStyle == textStyle) &&
190
+        (otherTheme.padding == padding);
191
+  }
192
+
193
+  @override
194
+  int get hashCode => hashValues(textStyle, padding);
195
+}
196
+
197
+/// Holds typography values for a block of lines in Zefyr editor.
198
+@immutable
199
+class BlockTheme {
200
+  /// Default text style for all text within a block, can be null.
201
+  ///
202
+  /// Takes precedence over line-level text style set by [LineTheme] if
203
+  /// [inheritLineTextStyle] is set to `false`. Otherwise this text style
204
+  /// is merged with the line's style.
205
+  final TextStyle textStyle;
206
+
207
+  /// Whether [textStyle] specified by this block theme should be merged with
208
+  /// text style of each individual line.
209
+  ///
210
+  /// Only applicable if [textStyle] is not null.
211
+  ///
212
+  /// If set to `true` then [textStyle] is merged with text style specified
213
+  /// by [LineTheme] of each line within a block. Otherwise [textStyle]
214
+  /// takes precedence and replaces style of [LineTheme].
215
+  final bool inheritLineTextStyle;
216
+
217
+  /// Space around the block.
218
+  final EdgeInsets padding;
219
+
220
+  /// Space around each individual line within a block, can be null.
221
+  ///
222
+  /// Takes precedence over line padding set in [LineTheme].
223
+  final EdgeInsets linePadding;
224
+
225
+  /// Creates a [BlockTheme] given a set of exact values.
226
+  const BlockTheme({
227
+    this.textStyle,
228
+    this.inheritLineTextStyle = true,
229
+    this.padding,
230
+    this.linePadding,
169 231
   });
170 232
 
171
-  /// Creates fallback theme for headings.
172
-  factory HeadingTheme.fallback() {
173
-    return HeadingTheme(
174
-      level1: StyleTheme(
175
-        textStyle: TextStyle(
176
-          fontSize: 30.0,
177
-          color: Colors.grey.shade800,
178
-          height: 1.25,
179
-          fontWeight: FontWeight.w600,
180
-        ),
181
-        padding: EdgeInsets.only(top: 16.0, bottom: 16.0),
182
-      ),
183
-      level2: StyleTheme(
184
-        textStyle: TextStyle(
185
-          fontSize: 24.0,
186
-          color: Colors.grey.shade800,
187
-          height: 1.25,
188
-          fontWeight: FontWeight.w600,
189
-        ),
190
-        padding: EdgeInsets.only(bottom: 8.0, top: 8.0),
191
-      ),
192
-      level3: StyleTheme(
193
-        textStyle: TextStyle(
194
-          fontSize: 20.0,
195
-          color: Colors.grey.shade800,
196
-          height: 1.25,
197
-          fontWeight: FontWeight.w600,
198
-        ),
199
-        padding: EdgeInsets.only(bottom: 8.0, top: 8.0),
200
-      ),
233
+  /// Creates a copy of this theme but with the given fields replaced with
234
+  /// the new values.
235
+  BlockTheme copyWith({
236
+    TextStyle textStyle,
237
+    EdgeInsets padding,
238
+    bool inheritLineTextStyle,
239
+    EdgeInsets linePadding,
240
+  }) {
241
+    return BlockTheme(
242
+      textStyle: textStyle ?? this.textStyle,
243
+      inheritLineTextStyle: inheritLineTextStyle ?? this.inheritLineTextStyle,
244
+      padding: padding ?? this.padding,
245
+      linePadding: linePadding ?? this.linePadding,
201 246
     );
202 247
   }
248
+
249
+  /// Creates a new [BlockTheme] where each property from this object has
250
+  /// been merged with the matching property from the `other` object.
251
+  ///
252
+  /// Text style is merged using [TextStyle.merge] when this and other
253
+  /// theme have this field set.
254
+  ///
255
+  /// If padding property is set in other then it replaces value of this
256
+  /// theme. [linePadding] follows the same logic.
257
+  BlockTheme merge(BlockTheme other) {
258
+    if (other == null) return this;
259
+    return copyWith(
260
+      textStyle: textStyle?.merge(other.textStyle) ?? other.textStyle,
261
+      inheritLineTextStyle: other.inheritLineTextStyle ?? inheritLineTextStyle,
262
+      padding: other.padding ?? padding,
263
+      linePadding: other.linePadding ?? linePadding,
264
+    );
265
+  }
266
+
267
+  @override
268
+  bool operator ==(other) {
269
+    if (other.runtimeType != runtimeType) return false;
270
+    final BlockTheme otherTheme = other;
271
+    return (otherTheme.textStyle == textStyle) &&
272
+        (otherTheme.inheritLineTextStyle == inheritLineTextStyle) &&
273
+        (otherTheme.padding == padding) &&
274
+        (otherTheme.linePadding == linePadding);
275
+  }
276
+
277
+  @override
278
+  int get hashCode =>
279
+      hashValues(textStyle, inheritLineTextStyle, padding, linePadding);
203 280
 }
204 281
 
205
-/// Theme for a block of lines in a document.
206
-class BlockTheme {
207
-  /// Style theme for bullet lists.
208
-  final StyleTheme bulletList;
282
+/// Holds style information for all format attributes supported by Zefyr editor.
283
+@immutable
284
+class AttributeTheme {
285
+  /// Style used to render "bold" text.
286
+  final TextStyle bold;
209 287
 
210
-  /// Style theme for number lists.
211
-  final StyleTheme numberList;
288
+  /// Style used to render "italic" text.
289
+  final TextStyle italic;
212 290
 
213
-  /// Style theme for code snippets.
214
-  final StyleTheme code;
291
+  /// Style used to render text containing links.
292
+  final TextStyle link;
215 293
 
216
-  /// Style theme for quotes.
217
-  final StyleTheme quote;
294
+  /// Style theme used to render largest headings.
295
+  final LineTheme heading1;
218 296
 
219
-  BlockTheme({
220
-    @required this.bulletList,
221
-    @required this.numberList,
222
-    @required this.quote,
223
-    @required this.code,
297
+  /// Style theme used to render medium headings.
298
+  final LineTheme heading2;
299
+
300
+  /// Style theme used to render smaller headings.
301
+  final LineTheme heading3;
302
+
303
+  /// Style theme used to render bullet lists.
304
+  final BlockTheme bulletList;
305
+
306
+  /// Style theme used to render number lists.
307
+  final BlockTheme numberList;
308
+
309
+  /// Style theme used to render quote blocks.
310
+  final BlockTheme quote;
311
+
312
+  /// Style theme used to render code blocks.
313
+  final BlockTheme code;
314
+
315
+  /// Creates a [AttributeTheme] given a set of exact values.
316
+  AttributeTheme({
317
+    this.bold,
318
+    this.italic,
319
+    this.link,
320
+    this.heading1,
321
+    this.heading2,
322
+    this.heading3,
323
+    this.bulletList,
324
+    this.numberList,
325
+    this.quote,
326
+    this.code,
224 327
   });
225 328
 
226
-  /// Creates fallback theme for blocks.
227
-  factory BlockTheme.fallback(ThemeData themeData) {
228
-    final padding = const EdgeInsets.only(bottom: 8.0);
229
-    String fontFamily;
230
-    switch (themeData.platform) {
329
+  /// The default attribute theme.
330
+  factory AttributeTheme.fallback(
331
+      BuildContext context, LineTheme defaultLineTheme) {
332
+    final theme = Theme.of(context);
333
+
334
+    String monospaceFontFamily;
335
+    switch (theme.platform) {
231 336
       case TargetPlatform.iOS:
232
-        fontFamily = 'Menlo';
337
+        monospaceFontFamily = 'Menlo';
233 338
         break;
234 339
       case TargetPlatform.android:
235 340
       case TargetPlatform.fuchsia:
236
-        fontFamily = 'Roboto Mono';
341
+        monospaceFontFamily = 'Roboto Mono';
237 342
         break;
343
+      default:
344
+        throw UnimplementedError("Platform ${theme.platform} not implemented.");
238 345
     }
239 346
 
240
-    return BlockTheme(
241
-      bulletList: StyleTheme(padding: padding),
242
-      numberList: StyleTheme(padding: padding),
243
-      quote: StyleTheme(
244
-        textStyle: TextStyle(color: Colors.grey.shade700),
245
-        padding: padding,
347
+    return AttributeTheme(
348
+      bold: TextStyle(fontWeight: FontWeight.bold),
349
+      italic: TextStyle(fontStyle: FontStyle.italic),
350
+      link: TextStyle(
351
+        decoration: TextDecoration.underline,
352
+        color: theme.accentColor,
353
+      ),
354
+      heading1: LineTheme(
355
+        textStyle: defaultLineTheme.textStyle.copyWith(
356
+          fontSize: 34.0,
357
+          color: defaultLineTheme.textStyle.color.withOpacity(0.7),
358
+          height: 1.15,
359
+          fontWeight: FontWeight.w300,
360
+        ),
361
+        padding: EdgeInsets.only(top: 16.0),
362
+      ),
363
+      heading2: LineTheme(
364
+        textStyle: defaultLineTheme.textStyle.copyWith(
365
+          fontSize: 24.0,
366
+          color: defaultLineTheme.textStyle.color.withOpacity(0.7),
367
+          height: 1.15,
368
+          fontWeight: FontWeight.normal,
369
+        ),
370
+        padding: EdgeInsets.only(top: 8.0),
371
+      ),
372
+      heading3: LineTheme(
373
+        textStyle: defaultLineTheme.textStyle.copyWith(
374
+          fontSize: 20.0,
375
+          color: defaultLineTheme.textStyle.color.withOpacity(0.7),
376
+          height: 1.15,
377
+          fontWeight: FontWeight.w500,
378
+        ),
379
+        padding: EdgeInsets.only(top: 8.0),
380
+      ),
381
+      bulletList: BlockTheme(
382
+        padding: EdgeInsets.symmetric(vertical: 8.0),
383
+        linePadding: EdgeInsets.symmetric(vertical: 2.0),
384
+      ),
385
+      numberList: BlockTheme(
386
+        padding: EdgeInsets.symmetric(vertical: 8.0),
387
+        linePadding: EdgeInsets.symmetric(vertical: 2.0),
246 388
       ),
247
-      code: StyleTheme(
389
+      quote: BlockTheme(
390
+        padding: EdgeInsets.symmetric(vertical: 8.0),
248 391
         textStyle: TextStyle(
249
-          color: Colors.blueGrey.shade800,
250
-          fontFamily: fontFamily,
392
+          color: defaultLineTheme.textStyle.color.withOpacity(0.6),
393
+        ),
394
+        inheritLineTextStyle: true,
395
+      ),
396
+      code: BlockTheme(
397
+        padding: EdgeInsets.symmetric(vertical: 8.0),
398
+        textStyle: TextStyle(
399
+          fontFamily: monospaceFontFamily,
251 400
           fontSize: 14.0,
401
+          color: defaultLineTheme.textStyle.color.withOpacity(0.8),
252 402
           height: 1.25,
253 403
         ),
254
-        padding: padding,
404
+        inheritLineTextStyle: false,
405
+        linePadding: EdgeInsets.zero,
255 406
       ),
256 407
     );
257 408
   }
258
-}
259 409
 
260
-/// Theme for a specific attribute style.
261
-///
262
-/// Used in [HeadingTheme] and [BlockTheme], as well as in
263
-/// [ZefyrThemeData.paragraphTheme].
264
-class StyleTheme {
265
-  /// Text style of this theme.
266
-  final TextStyle textStyle;
410
+  /// Creates a new [AttributeTheme] where each property from this object has
411
+  /// been merged with the matching property from the `other` object.
412
+  AttributeTheme copyWith({
413
+    TextStyle bold,
414
+    TextStyle italic,
415
+    TextStyle link,
416
+    LineTheme heading1,
417
+    LineTheme heading2,
418
+    LineTheme heading3,
419
+    BlockTheme bulletList,
420
+    BlockTheme numberList,
421
+    BlockTheme quote,
422
+    BlockTheme code,
423
+  }) {
424
+    return AttributeTheme(
425
+      bold: bold ?? this.bold,
426
+      italic: italic ?? this.italic,
427
+      link: link ?? this.link,
428
+      heading1: heading1 ?? this.heading1,
429
+      heading2: heading2 ?? this.heading2,
430
+      heading3: heading3 ?? this.heading3,
431
+      bulletList: bulletList ?? this.bulletList,
432
+      numberList: numberList ?? this.numberList,
433
+      quote: quote ?? this.quote,
434
+      code: code ?? this.code,
435
+    );
436
+  }
267 437
 
268
-  /// Padding to apply around lines of text.
269
-  final EdgeInsets padding;
438
+  /// Creates a new [AttributeTheme] where each property from this object has
439
+  /// been merged with the matching property from the `other` object.
440
+  AttributeTheme merge(AttributeTheme other) {
441
+    if (other == null) return this;
442
+    return copyWith(
443
+      bold: bold?.merge(other.bold) ?? other.bold,
444
+      italic: italic?.merge(other.italic) ?? other.italic,
445
+      link: link?.merge(other.link) ?? other.link,
446
+      heading1: heading1?.merge(other.heading1) ?? other.heading1,
447
+      heading2: heading2?.merge(other.heading2) ?? other.heading2,
448
+      heading3: heading3?.merge(other.heading3) ?? other.heading3,
449
+      bulletList: bulletList?.merge(other.bulletList) ?? other.bulletList,
450
+      numberList: numberList?.merge(other.numberList) ?? other.numberList,
451
+      quote: quote?.merge(other.quote) ?? other.quote,
452
+      code: code?.merge(other.code) ?? other.code,
453
+    );
454
+  }
270 455
 
271
-  /// Creates a new [StyleTheme].
272
-  StyleTheme({
273
-    this.textStyle,
274
-    this.padding,
275
-  });
456
+  @override
457
+  bool operator ==(other) {
458
+    if (other.runtimeType != runtimeType) return false;
459
+    final AttributeTheme otherTheme = other;
460
+    return (otherTheme.bold == bold) &&
461
+        (otherTheme.italic == italic) &&
462
+        (otherTheme.link == link) &&
463
+        (otherTheme.heading1 == heading1) &&
464
+        (otherTheme.heading2 == heading2) &&
465
+        (otherTheme.heading3 == heading3) &&
466
+        (otherTheme.bulletList == bulletList) &&
467
+        (otherTheme.numberList == numberList) &&
468
+        (otherTheme.quote == quote) &&
469
+        (otherTheme.code == code);
470
+  }
471
+
472
+  @override
473
+  int get hashCode {
474
+    return hashList([
475
+      bold,
476
+      italic,
477
+      link,
478
+      heading1,
479
+      heading2,
480
+      heading3,
481
+      bulletList,
482
+      numberList,
483
+      quote,
484
+      code,
485
+    ]);
486
+  }
276 487
 }
277 488
 
278
-/// Defines styles and colors for [ZefyrToolbar].
279
-class ZefyrToolbarTheme {
280
-  /// The background color of toolbar.
489
+/// Defines styles and colors for Zefyr editor toolbar.
490
+class ToolbarTheme {
491
+  /// The background color of the toolbar.
281 492
   final Color color;
282 493
 
283 494
   /// Color of buttons in toggled state.
@@ -289,35 +500,67 @@ class ZefyrToolbarTheme {
289 500
   /// Color of button icons in disabled state.
290 501
   final Color disabledIconColor;
291 502
 
292
-  /// Creates fallback theme for editor toolbars.
293
-  factory ZefyrToolbarTheme.fallback(BuildContext context) {
503
+  /// Creates default theme for editor toolbar.
504
+  factory ToolbarTheme.fallback(BuildContext context) {
294 505
     final theme = Theme.of(context);
295
-    return ZefyrToolbarTheme._(
296
-      color: theme.primaryColorLight,
297
-      toggleColor: theme.primaryColor,
506
+    return ToolbarTheme._(
507
+      color: theme.primaryColorBrightness == Brightness.light
508
+          ? Colors.grey.shade300
509
+          : Colors.grey.shade800,
510
+      toggleColor: theme.primaryColorBrightness == Brightness.light
511
+          ? Colors.grey.shade400
512
+          : Colors.grey.shade900,
298 513
       iconColor: theme.primaryIconTheme.color,
299
-      disabledIconColor: theme.primaryColor,
514
+      disabledIconColor: theme.disabledColor,
300 515
     );
301 516
   }
302 517
 
303
-  ZefyrToolbarTheme._({
518
+  ToolbarTheme._({
304 519
     @required this.color,
305 520
     @required this.toggleColor,
306 521
     @required this.iconColor,
307 522
     @required this.disabledIconColor,
308 523
   });
309 524
 
310
-  ZefyrToolbarTheme copyWith({
525
+  /// Creates a new [ToolbarTheme] where each property from this object has
526
+  /// been merged with the matching property from the `other` object.
527
+  ToolbarTheme copyWith({
311 528
     Color color,
312 529
     Color toggleColor,
313 530
     Color iconColor,
314 531
     Color disabledIconColor,
315 532
   }) {
316
-    return ZefyrToolbarTheme._(
533
+    return ToolbarTheme._(
317 534
       color: color ?? this.color,
318 535
       toggleColor: toggleColor ?? this.toggleColor,
319 536
       iconColor: iconColor ?? this.iconColor,
320 537
       disabledIconColor: disabledIconColor ?? this.disabledIconColor,
321 538
     );
322 539
   }
540
+
541
+  /// Creates a new [ToolbarTheme] where each property from this object has
542
+  /// been merged with the matching property from the `other` object.
543
+  ToolbarTheme merge(ToolbarTheme other) {
544
+    if (other == null) return this;
545
+    return copyWith(
546
+      color: other.color ?? color,
547
+      toggleColor: other.toggleColor ?? toggleColor,
548
+      iconColor: other.iconColor ?? iconColor,
549
+      disabledIconColor: other.disabledIconColor ?? disabledIconColor,
550
+    );
551
+  }
552
+
553
+  @override
554
+  bool operator ==(other) {
555
+    if (other.runtimeType != runtimeType) return false;
556
+    final ToolbarTheme otherTheme = other;
557
+    return (otherTheme.color == color) &&
558
+        (otherTheme.toggleColor == toggleColor) &&
559
+        (otherTheme.iconColor == iconColor) &&
560
+        (otherTheme.disabledIconColor == disabledIconColor);
561
+  }
562
+
563
+  @override
564
+  int get hashCode =>
565
+      hashValues(color, toggleColor, iconColor, disabledIconColor);
323 566
 }

+ 1
- 1
packages/zefyr/lib/src/widgets/view.dart Vedi File

@@ -86,7 +86,7 @@ class ZefyrViewState extends State<ZefyrView> {
86 86
   Widget _defaultChildBuilder(BuildContext context, Node node) {
87 87
     if (node is LineNode) {
88 88
       if (node.hasEmbed) {
89
-        return RawZefyrLine(node: node);
89
+        return ZefyrLine(node: node);
90 90
       } else if (node.style.contains(NotusAttribute.heading)) {
91 91
         return ZefyrHeading(node: node);
92 92
       }

+ 1
- 1
packages/zefyr/pubspec.yaml Vedi File

@@ -1,6 +1,6 @@
1 1
 name: zefyr
2 2
 description: Clean, minimalistic and collaboration-ready rich text editor for Flutter.
3
-version: 0.9.1
3
+version: 0.10.0
4 4
 author: Anatoly Pulyaevskiy <anatoly.pulyaevskiy@gmail.com>
5 5
 homepage: https://github.com/memspace/zefyr
6 6
 

+ 3
- 2
packages/zefyr/test/widgets/editor_test.dart Vedi File

@@ -11,7 +11,7 @@ import '../testing.dart';
11 11
 
12 12
 void main() {
13 13
   group('$ZefyrEditor', () {
14
-    testWidgets('allows merging theme data', (tester) async {
14
+    testWidgets('allows merging attribute theme data', (tester) async {
15 15
       var delta = Delta()
16 16
         ..insert(
17 17
           'Website',
@@ -19,7 +19,8 @@ void main() {
19 19
         )
20 20
         ..insert('\n');
21 21
       var doc = NotusDocument.fromDelta(delta);
22
-      var theme = ZefyrThemeData(linkStyle: TextStyle(color: Colors.red));
22
+      var attributeTheme = AttributeTheme(link: TextStyle(color: Colors.red));
23
+      var theme = ZefyrThemeData(attributeTheme: attributeTheme);
23 24
       var editor = EditorSandBox(tester: tester, document: doc, theme: theme);
24 25
       await editor.pumpAndTap();
25 26
       // TODO: figure out why this extra pump is needed here