Browse Source

Theming layer rewrite and Dark Mode support (#232)

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

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

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
 ## 0.9.1
21
 ## 0.9.1
2
 
22
 
3
 * Added: Support for iOS keyboard appearance. See `ZefyrEditor.keyboardAppearance` and `ZefyrField.keyboardAppearance`
23
 * Added: Support for iOS keyboard appearance. See `ZefyrEditor.keyboardAppearance` and `ZefyrField.keyboardAppearance`

+ 5
- 16
packages/zefyr/example/lib/main.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/material.dart';
4
 import 'package:flutter/material.dart';
5
+
5
 import 'src/form.dart';
6
 import 'src/form.dart';
6
 import 'src/full_page.dart';
7
 import 'src/full_page.dart';
7
 import 'src/view.dart';
8
 import 'src/view.dart';
16
     return MaterialApp(
17
     return MaterialApp(
17
       debugShowCheckedModeBanner: false,
18
       debugShowCheckedModeBanner: false,
18
       title: 'Zefyr Editor',
19
       title: 'Zefyr Editor',
19
-      theme: ThemeData(primarySwatch: Colors.cyan),
20
       home: HomePage(),
20
       home: HomePage(),
21
       routes: {
21
       routes: {
22
         "/fullPage": buildFullPage,
22
         "/fullPage": buildFullPage,
44
   Widget build(BuildContext context) {
44
   Widget build(BuildContext context) {
45
     final nav = Navigator.of(context);
45
     final nav = Navigator.of(context);
46
     return Scaffold(
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
       body: Column(
48
       body: Column(
54
         children: <Widget>[
49
         children: <Widget>[
55
           Expanded(child: Container()),
50
           Expanded(child: Container()),
56
-          FlatButton(
51
+          RaisedButton(
57
             onPressed: () => nav.pushNamed('/fullPage'),
52
             onPressed: () => nav.pushNamed('/fullPage'),
58
             child: Text('Full page editor'),
53
             child: Text('Full page editor'),
59
-            color: Colors.lightBlue,
60
-            textColor: Colors.white,
61
           ),
54
           ),
62
-          FlatButton(
55
+          RaisedButton(
63
             onPressed: () => nav.pushNamed('/form'),
56
             onPressed: () => nav.pushNamed('/form'),
64
             child: Text('Embedded in a form'),
57
             child: Text('Embedded in a form'),
65
-            color: Colors.lightBlue,
66
-            textColor: Colors.white,
67
           ),
58
           ),
68
-          FlatButton(
59
+          RaisedButton(
69
             onPressed: () => nav.pushNamed('/view'),
60
             onPressed: () => nav.pushNamed('/view'),
70
             child: Text('Read-only embeddable view'),
61
             child: Text('Read-only embeddable view'),
71
-            color: Colors.lightBlue,
72
-            textColor: Colors.white,
73
           ),
62
           ),
74
           Expanded(child: Container()),
63
           Expanded(child: Container()),
75
         ],
64
         ],

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

8
 import 'full_page.dart';
8
 import 'full_page.dart';
9
 import 'images.dart';
9
 import 'images.dart';
10
 
10
 
11
+enum _Options { darkTheme }
12
+
11
 class FormEmbeddedScreen extends StatefulWidget {
13
 class FormEmbeddedScreen extends StatefulWidget {
12
   @override
14
   @override
13
   _FormEmbeddedScreenState createState() => _FormEmbeddedScreenState();
15
   _FormEmbeddedScreenState createState() => _FormEmbeddedScreenState();
17
   final ZefyrController _controller = ZefyrController(NotusDocument());
19
   final ZefyrController _controller = ZefyrController(NotusDocument());
18
   final FocusNode _focusNode = FocusNode();
20
   final FocusNode _focusNode = FocusNode();
19
 
21
 
22
+  bool _darkTheme = false;
23
+
20
   @override
24
   @override
21
   Widget build(BuildContext context) {
25
   Widget build(BuildContext context) {
22
     final form = ListView(
26
     final form = ListView(
27
       ],
31
       ],
28
     );
32
     );
29
 
33
 
30
-    return Scaffold(
34
+    final result = Scaffold(
31
       resizeToAvoidBottomPadding: true,
35
       resizeToAvoidBottomPadding: true,
32
       appBar: AppBar(
36
       appBar: AppBar(
33
-        elevation: 1.0,
34
-        backgroundColor: Colors.grey.shade200,
35
-        brightness: Brightness.light,
36
         title: ZefyrLogo(),
37
         title: ZefyrLogo(),
38
+        actions: [
39
+          PopupMenuButton<_Options>(
40
+            itemBuilder: buildPopupMenu,
41
+            onSelected: handlePopupItemSelected,
42
+          )
43
+        ],
37
       ),
44
       ),
38
       body: ZefyrScaffold(
45
       body: ZefyrScaffold(
39
         child: Padding(
46
         child: Padding(
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
   Widget buildEditor() {
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 View File

41
   return Delta.fromJson(json.decode(doc) as List);
41
   return Delta.fromJson(json.decode(doc) as List);
42
 }
42
 }
43
 
43
 
44
+enum _Options { darkTheme }
45
+
44
 class _FullPageEditorScreenState extends State<FullPageEditorScreen> {
46
 class _FullPageEditorScreenState extends State<FullPageEditorScreen> {
45
   final ZefyrController _controller =
47
   final ZefyrController _controller =
46
       ZefyrController(NotusDocument.fromDelta(getDelta()));
48
       ZefyrController(NotusDocument.fromDelta(getDelta()));
47
   final FocusNode _focusNode = FocusNode();
49
   final FocusNode _focusNode = FocusNode();
48
   bool _editing = false;
50
   bool _editing = false;
49
   StreamSubscription<NotusChange> _sub;
51
   StreamSubscription<NotusChange> _sub;
52
+  bool _darkTheme = false;
50
 
53
 
51
   @override
54
   @override
52
   void initState() {
55
   void initState() {
64
 
67
 
65
   @override
68
   @override
66
   Widget build(BuildContext context) {
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
     final done = _editing
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
       resizeToAvoidBottomPadding: true,
74
       resizeToAvoidBottomPadding: true,
82
       appBar: AppBar(
75
       appBar: AppBar(
83
-        elevation: 1.0,
84
-        backgroundColor: Colors.grey.shade200,
85
-        brightness: Brightness.light,
86
         title: ZefyrLogo(),
76
         title: ZefyrLogo(),
87
-        actions: done,
77
+        actions: [
78
+          done,
79
+          PopupMenuButton<_Options>(
80
+            itemBuilder: buildPopupMenu,
81
+            onSelected: handlePopupItemSelected,
82
+          )
83
+        ],
88
       ),
84
       ),
89
       body: ZefyrScaffold(
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
   void _startEditing() {
120
   void _startEditing() {

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

32
 
32
 
33
   @override
33
   @override
34
   Widget build(BuildContext context) {
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
     return Scaffold(
35
     return Scaffold(
44
       resizeToAvoidBottomPadding: true,
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 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/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 View File

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

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

16
 
16
 
17
   @override
17
   @override
18
   Widget build(BuildContext context) {
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
     List<Widget> items = [];
22
     List<Widget> items = [];
22
     for (var line in node.children) {
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
     return Padding(
31
     return Padding(
27
-      padding: theme.blockTheme.code.padding,
32
+      padding: zefyrTheme.attributeTheme.code.padding,
28
       child: Container(
33
       child: Container(
29
-        // TODO: make decorations configurable
30
         decoration: BoxDecoration(
34
         decoration: BoxDecoration(
31
-          color: Colors.blueGrey.shade50,
35
+          color: color,
32
           borderRadius: BorderRadius.circular(3.0),
36
           borderRadius: BorderRadius.circular(3.0),
33
         ),
37
         ),
34
         padding: const EdgeInsets.all(16.0),
38
         padding: const EdgeInsets.all(16.0),
42
 
46
 
43
   Widget _buildLine(Node node, TextStyle style) {
47
   Widget _buildLine(Node node, TextStyle style) {
44
     LineNode line = node;
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 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 'package:flutter/cupertino.dart';
5
+import 'package:flutter/material.dart';
4
 import 'package:flutter/rendering.dart';
6
 import 'package:flutter/rendering.dart';
5
 import 'package:flutter/widgets.dart';
7
 import 'package:flutter/widgets.dart';
6
 import 'package:notus/notus.dart';
8
 import 'package:notus/notus.dart';
12
 import 'scope.dart';
14
 import 'scope.dart';
13
 import 'theme.dart';
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
   /// Line in the document represented by this widget.
23
   /// Line in the document represented by this widget.
28
   final LineNode node;
24
   final LineNode node;
35
   final EdgeInsets padding;
31
   final EdgeInsets padding;
36
 
32
 
37
   @override
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
   final LayerLink _link = LayerLink();
38
   final LayerLink _link = LayerLink();
43
 
39
 
44
   @override
40
   @override
47
     if (scope.isEditable) {
43
     if (scope.isEditable) {
48
       ensureVisible(context, scope);
44
       ensureVisible(context, scope);
49
     }
45
     }
50
-    final theme = ZefyrTheme.of(context);
46
+    final theme = Theme.of(context);
51
 
47
 
52
     Widget content;
48
     Widget content;
53
     if (widget.node.hasEmbed) {
49
     if (widget.node.hasEmbed) {
61
     }
57
     }
62
 
58
 
63
     if (scope.isEditable) {
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
       content = EditableBox(
72
       content = EditableBox(
65
         child: content,
73
         child: content,
66
         node: widget.node,
74
         node: widget.node,
68
         renderContext: scope.renderContext,
76
         renderContext: scope.renderContext,
69
         showCursor: scope.showCursor,
77
         showCursor: scope.showCursor,
70
         selection: scope.selection,
78
         selection: scope.selection,
71
-        selectionColor: theme.selectionColor,
72
-        cursorColor: theme.cursorColor,
79
+        selectionColor: theme.textSelectionColor,
80
+        cursorColor: cursorColor,
73
       );
81
       );
74
       content = CompositedTransformTarget(link: _link, child: content);
82
       content = CompositedTransformTarget(link: _link, child: content);
75
     }
83
     }
129
   TextStyle _getTextStyle(NotusStyle style, ZefyrThemeData theme) {
137
   TextStyle _getTextStyle(NotusStyle style, ZefyrThemeData theme) {
130
     TextStyle result = TextStyle();
138
     TextStyle result = TextStyle();
131
     if (style.containsSame(NotusAttribute.bold)) {
139
     if (style.containsSame(NotusAttribute.bold)) {
132
-      result = result.merge(theme.boldStyle);
140
+      result = result.merge(theme.attributeTheme.bold);
133
     }
141
     }
134
     if (style.containsSame(NotusAttribute.italic)) {
142
     if (style.containsSame(NotusAttribute.italic)) {
135
-      result = result.merge(theme.italicStyle);
143
+      result = result.merge(theme.attributeTheme.italic);
136
     }
144
     }
137
     if (style.contains(NotusAttribute.link)) {
145
     if (style.contains(NotusAttribute.link)) {
138
-      result = result.merge(theme.linkStyle);
146
+      result = result.merge(theme.attributeTheme.link);
139
     }
147
     }
140
     return result;
148
     return result;
141
   }
149
   }

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

72
     @required TextSelection selection,
72
     @required TextSelection selection,
73
     @required Color selectionColor,
73
     @required Color selectionColor,
74
     @required Color cursorColor,
74
     @required Color cursorColor,
75
-  })  : _node = node,
75
+  })  : node = node,
76
         _layerLink = layerLink,
76
         _layerLink = layerLink,
77
         _renderContext = renderContext,
77
         _renderContext = renderContext,
78
         _showCursor = showCursor,
78
         _showCursor = showCursor,
94
 
94
 
95
   bool _isDirty = true;
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
   LayerLink get layerLink => _layerLink;
99
   LayerLink get layerLink => _layerLink;
104
   LayerLink _layerLink;
100
   LayerLink _layerLink;
153
   /// Returns `true` if current selection is collapsed and located
149
   /// Returns `true` if current selection is collapsed and located
154
   /// within this paragraph.
150
   /// within this paragraph.
155
   bool get containsCaret {
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
     if (!_selection.isCollapsed) return false;
159
     if (!_selection.isCollapsed) return false;
157
 
160
 
158
     final int start = node.documentOffset;
161
     final int start = node.documentOffset;

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

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

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

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

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

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

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

27
     final isNumberList =
27
     final isNumberList =
28
         node.style.get(NotusAttribute.block) == NotusAttribute.block.numberList;
28
         node.style.get(NotusAttribute.block) == NotusAttribute.block.numberList;
29
     EdgeInsets padding = isNumberList
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
     return Padding(
34
     return Padding(
35
       padding: padding,
35
       padding: padding,
55
     final BlockNode block = node.parent;
55
     final BlockNode block = node.parent;
56
     final style = block.style.get(NotusAttribute.block);
56
     final style = block.style.get(NotusAttribute.block);
57
     final theme = ZefyrTheme.of(context);
57
     final theme = ZefyrTheme.of(context);
58
+    final blockTheme = (style == NotusAttribute.block.bulletList)
59
+        ? theme.attributeTheme.bulletList
60
+        : theme.attributeTheme.numberList;
58
     final bulletText =
61
     final bulletText =
59
         (style == NotusAttribute.block.bulletList) ? '•' : '$index.';
62
         (style == NotusAttribute.block.bulletList) ? '•' : '$index.';
60
 
63
 
68
       padding = headingTheme.padding;
71
       padding = headingTheme.padding;
69
       content = ZefyrHeading(node: node);
72
       content = ZefyrHeading(node: node);
70
     } else {
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
     Widget bullet =
83
     Widget bullet =

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

18
   @override
18
   @override
19
   Widget build(BuildContext context) {
19
   Widget build(BuildContext context) {
20
     final theme = ZefyrTheme.of(context);
20
     final theme = ZefyrTheme.of(context);
21
-    TextStyle style = theme.paragraphTheme.textStyle;
21
+    TextStyle style = theme.defaultLineTheme.textStyle;
22
     if (blockStyle != null) {
22
     if (blockStyle != null) {
23
       style = style.merge(blockStyle);
23
       style = style.merge(blockStyle);
24
     }
24
     }
25
-    return RawZefyrLine(
25
+    return ZefyrLine(
26
       node: node,
26
       node: node,
27
       style: style,
27
       style: style,
28
-      padding: theme.paragraphTheme.padding,
28
+      padding: theme.defaultLineTheme.padding,
29
     );
29
     );
30
   }
30
   }
31
 }
31
 }
46
     if (blockStyle != null) {
46
     if (blockStyle != null) {
47
       style = style.merge(blockStyle);
47
       style = style.merge(blockStyle);
48
     }
48
     }
49
-    return RawZefyrLine(
49
+    return ZefyrLine(
50
       node: node,
50
       node: node,
51
       style: style,
51
       style: style,
52
       padding: theme.padding,
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
     final theme = ZefyrTheme.of(context);
57
     final theme = ZefyrTheme.of(context);
58
     final style = node.style.get(NotusAttribute.heading);
58
     final style = node.style.get(NotusAttribute.heading);
59
     if (style == NotusAttribute.heading.level1) {
59
     if (style == NotusAttribute.heading.level1) {
60
-      return theme.headingTheme.level1;
60
+      return theme.attributeTheme.heading1;
61
     } else if (style == NotusAttribute.heading.level2) {
61
     } else if (style == NotusAttribute.heading.level2) {
62
-      return theme.headingTheme.level2;
62
+      return theme.attributeTheme.heading2;
63
     } else if (style == NotusAttribute.heading.level3) {
63
     } else if (style == NotusAttribute.heading.level3) {
64
-      return theme.headingTheme.level3;
64
+      return theme.attributeTheme.heading3;
65
     }
65
     }
66
     throw UnimplementedError('Unsupported heading style $style');
66
     throw UnimplementedError('Unsupported heading style $style');
67
   }
67
   }

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

16
   @override
16
   @override
17
   Widget build(BuildContext context) {
17
   Widget build(BuildContext context) {
18
     final theme = ZefyrTheme.of(context);
18
     final theme = ZefyrTheme.of(context);
19
-    final style = theme.blockTheme.quote.textStyle;
19
+    final style = theme.attributeTheme.quote.textStyle;
20
     List<Widget> items = [];
20
     List<Widget> items = [];
21
     for (var line in node.children) {
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
     return Padding(
25
     return Padding(
26
-      padding: theme.blockTheme.quote.padding,
26
+      padding: theme.attributeTheme.quote.padding,
27
       child: Column(
27
       child: Column(
28
         crossAxisAlignment: CrossAxisAlignment.stretch,
28
         crossAxisAlignment: CrossAxisAlignment.stretch,
29
         children: items,
29
         children: items,

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

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

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

381
       // TODO: For some reason sometimes we get updates when render boxes
381
       // TODO: For some reason sometimes we get updates when render boxes
382
       //      are in process of rebuilding so we don't have access to them here.
382
       //      are in process of rebuilding so we don't have access to them here.
383
       //      As a workaround we just return empty container. There is usually
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
       //      Example: when toolbar button is toggled changing style of current
385
       //      Example: when toolbar button is toggled changing style of current
386
       //      selection.
386
       //      selection.
387
       return Container();
387
       return Container();

+ 433
- 190
packages/zefyr/lib/src/widgets/theme.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';
1
 import 'package:flutter/material.dart';
6
-import 'package:flutter/widgets.dart';
7
-import 'package:meta/meta.dart';
8
 
2
 
9
 /// Applies a Zefyr editor theme to descendant widgets.
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
 /// Descendant widgets obtain the current theme's [ZefyrThemeData] object using
7
 /// Descendant widgets obtain the current theme's [ZefyrThemeData] object using
14
 /// [ZefyrTheme.of].
8
 /// [ZefyrTheme.of].
15
-///
16
-/// See also:
17
-///
18
-///   * [ZefyrThemeData], which describes actual configuration of a theme.
19
 class ZefyrTheme extends InheritedWidget {
9
 class ZefyrTheme extends InheritedWidget {
20
   final ZefyrThemeData data;
10
   final ZefyrThemeData data;
21
 
11
 
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
 class ZefyrThemeData {
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
   factory ZefyrThemeData.fallback(BuildContext context) {
74
   factory ZefyrThemeData.fallback(BuildContext context) {
70
-    final ThemeData themeData = Theme.of(context);
71
     final defaultStyle = DefaultTextStyle.of(context);
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
     return ZefyrThemeData(
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
   ZefyrThemeData copyWith({
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
     return ZefyrThemeData(
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
       toolbarTheme: toolbarTheme ?? this.toolbarTheme,
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
   ZefyrThemeData merge(ZefyrThemeData other) {
109
   ZefyrThemeData merge(ZefyrThemeData other) {
110
+    if (other == null) return this;
139
     return copyWith(
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
       case TargetPlatform.iOS:
336
       case TargetPlatform.iOS:
232
-        fontFamily = 'Menlo';
337
+        monospaceFontFamily = 'Menlo';
233
         break;
338
         break;
234
       case TargetPlatform.android:
339
       case TargetPlatform.android:
235
       case TargetPlatform.fuchsia:
340
       case TargetPlatform.fuchsia:
236
-        fontFamily = 'Roboto Mono';
341
+        monospaceFontFamily = 'Roboto Mono';
237
         break;
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
         textStyle: TextStyle(
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
           fontSize: 14.0,
400
           fontSize: 14.0,
401
+          color: defaultLineTheme.textStyle.color.withOpacity(0.8),
252
           height: 1.25,
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
   final Color color;
492
   final Color color;
282
 
493
 
283
   /// Color of buttons in toggled state.
494
   /// Color of buttons in toggled state.
289
   /// Color of button icons in disabled state.
500
   /// Color of button icons in disabled state.
290
   final Color disabledIconColor;
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
     final theme = Theme.of(context);
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
       iconColor: theme.primaryIconTheme.color,
513
       iconColor: theme.primaryIconTheme.color,
299
-      disabledIconColor: theme.primaryColor,
514
+      disabledIconColor: theme.disabledColor,
300
     );
515
     );
301
   }
516
   }
302
 
517
 
303
-  ZefyrToolbarTheme._({
518
+  ToolbarTheme._({
304
     @required this.color,
519
     @required this.color,
305
     @required this.toggleColor,
520
     @required this.toggleColor,
306
     @required this.iconColor,
521
     @required this.iconColor,
307
     @required this.disabledIconColor,
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
     Color color,
528
     Color color,
312
     Color toggleColor,
529
     Color toggleColor,
313
     Color iconColor,
530
     Color iconColor,
314
     Color disabledIconColor,
531
     Color disabledIconColor,
315
   }) {
532
   }) {
316
-    return ZefyrToolbarTheme._(
533
+    return ToolbarTheme._(
317
       color: color ?? this.color,
534
       color: color ?? this.color,
318
       toggleColor: toggleColor ?? this.toggleColor,
535
       toggleColor: toggleColor ?? this.toggleColor,
319
       iconColor: iconColor ?? this.iconColor,
536
       iconColor: iconColor ?? this.iconColor,
320
       disabledIconColor: disabledIconColor ?? this.disabledIconColor,
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 View File

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

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

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

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

11
 
11
 
12
 void main() {
12
 void main() {
13
   group('$ZefyrEditor', () {
13
   group('$ZefyrEditor', () {
14
-    testWidgets('allows merging theme data', (tester) async {
14
+    testWidgets('allows merging attribute theme data', (tester) async {
15
       var delta = Delta()
15
       var delta = Delta()
16
         ..insert(
16
         ..insert(
17
           'Website',
17
           'Website',
19
         )
19
         )
20
         ..insert('\n');
20
         ..insert('\n');
21
       var doc = NotusDocument.fromDelta(delta);
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
       var editor = EditorSandBox(tester: tester, document: doc, theme: theme);
24
       var editor = EditorSandBox(tester: tester, document: doc, theme: theme);
24
       await editor.pumpAndTap();
25
       await editor.pumpAndTap();
25
       // TODO: figure out why this extra pump is needed here
26
       // TODO: figure out why this extra pump is needed here