Browse Source

Merge pull request #1364 from LucasXu0/adapt_dart_mode

feat: adapt dark mode
Lucas.Xu 2 years ago
parent
commit
c7c9048fe3
35 changed files with 921 additions and 534 deletions
  1. 6 1
      frontend/app_flowy/lib/plugins/doc/document_page.dart
  2. 51 47
      frontend/app_flowy/lib/plugins/doc/editor_styles.dart
  3. 4 2
      frontend/app_flowy/lib/plugins/doc/presentation/plugins/horizontal_rule_node_widget.dart
  4. 23 45
      frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart
  5. 2 2
      frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart
  6. 1 1
      frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/horizontal_rule_node_widget.dart
  7. 1 1
      frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart
  8. 2 0
      frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart
  9. 4 3
      frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart
  10. 10 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/theme_extension.dart
  11. 9 3
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart
  12. 12 4
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart
  13. 0 50
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/built_in_text_widget.dart
  14. 25 12
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart
  15. 23 13
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart
  16. 5 4
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart
  17. 18 2
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart
  18. 24 8
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart
  19. 25 7
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart
  20. 11 8
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart
  21. 31 13
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart
  22. 38 18
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart
  23. 2 2
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart
  24. 159 214
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart
  25. 327 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/plugin_styles.dart
  26. 2 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart
  27. 1 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart
  28. 36 19
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/context_menu/context_menu.dart
  29. 46 44
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart
  30. 17 4
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart
  31. 1 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/format_style_handler.dart
  32. 0 1
      frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart
  33. 3 2
      frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart
  34. 1 1
      frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart
  35. 1 1
      frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart

+ 6 - 1
frontend/app_flowy/lib/plugins/doc/document_page.dart

@@ -93,15 +93,20 @@ class _DocumentPageState extends State<DocumentPage> {
   }
   }
 
 
   Widget _renderAppFlowyEditor(EditorState editorState) {
   Widget _renderAppFlowyEditor(EditorState editorState) {
+    final theme = Theme.of(context);
     final editor = AppFlowyEditor(
     final editor = AppFlowyEditor(
       editorState: editorState,
       editorState: editorState,
-      editorStyle: customEditorStyle(context),
       customBuilders: {
       customBuilders: {
         'horizontal_rule': HorizontalRuleWidgetBuilder(),
         'horizontal_rule': HorizontalRuleWidgetBuilder(),
       },
       },
       shortcutEvents: [
       shortcutEvents: [
         insertHorizontalRule,
         insertHorizontalRule,
       ],
       ],
+      themeData: theme.copyWith(extensions: [
+        ...theme.extensions.values,
+        customEditorTheme(context),
+        ...customPluginTheme(context),
+      ]),
     );
     );
     return Expanded(
     return Expanded(
       child: SizedBox.expand(
       child: SizedBox.expand(

+ 51 - 47
frontend/app_flowy/lib/plugins/doc/editor_styles.dart

@@ -3,59 +3,63 @@ import 'package:flowy_infra/theme.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
 import 'package:provider/provider.dart';
 
 
-EditorStyle customEditorStyle(BuildContext context) {
+const _baseFontSize = 14.0;
+
+EditorStyle customEditorTheme(BuildContext context) {
   final theme = context.watch<AppTheme>();
   final theme = context.watch<AppTheme>();
-  const baseFontSize = 14.0;
-  const basePadding = 12.0;
-  var textStyle = theme.isDark
-      ? BuiltInTextStyle.builtInDarkMode()
-      : BuiltInTextStyle.builtIn();
-  textStyle = textStyle.copyWith(
-    defaultTextStyle: textStyle.defaultTextStyle.copyWith(
+
+  var editorStyle = theme.isDark ? EditorStyle.dark : EditorStyle.light;
+  editorStyle = editorStyle.copyWith(
+    textStyle: editorStyle.textStyle?.copyWith(
+      fontFamily: 'poppins',
+      fontSize: _baseFontSize,
+    ),
+    placeholderTextStyle: editorStyle.placeholderTextStyle?.copyWith(
       fontFamily: 'poppins',
       fontFamily: 'poppins',
-      fontSize: baseFontSize,
+      fontSize: _baseFontSize,
     ),
     ),
-    bold: textStyle.bold.copyWith(
+    bold: editorStyle.bold?.copyWith(
       fontWeight: FontWeight.w500,
       fontWeight: FontWeight.w500,
     ),
     ),
   );
   );
-  return EditorStyle.defaultStyle().copyWith(
-    padding: const EdgeInsets.symmetric(horizontal: 80),
-    textStyle: textStyle,
-    pluginStyles: {
-      'text/heading': builtInPluginStyle
-        ..update(
-          'textStyle',
-          (_) => (EditorState editorState, Node node) {
-            final headingToFontSize = {
-              'h1': baseFontSize + 12,
-              'h2': baseFontSize + 8,
-              'h3': baseFontSize + 4,
-              'h4': baseFontSize,
-              'h5': baseFontSize,
-              'h6': baseFontSize,
-            };
-            final fontSize =
-                headingToFontSize[node.attributes.heading] ?? baseFontSize;
-            return TextStyle(fontSize: fontSize, fontWeight: FontWeight.w600);
-          },
-        )
-        ..update(
-          'padding',
-          (_) => (EditorState editorState, Node node) {
-            final headingToPadding = {
-              'h1': basePadding + 6,
-              'h2': basePadding + 4,
-              'h3': basePadding + 2,
-              'h4': basePadding,
-              'h5': basePadding,
-              'h6': basePadding,
-            };
-            final padding =
-                headingToPadding[node.attributes.heading] ?? basePadding;
-            return EdgeInsets.only(bottom: padding);
-          },
-        )
+  return editorStyle;
+}
+
+Iterable<ThemeExtension<dynamic>> customPluginTheme(BuildContext context) {
+  final theme = context.watch<AppTheme>();
+  const basePadding = 12.0;
+  var headingPluginStyle =
+      theme.isDark ? HeadingPluginStyle.dark : HeadingPluginStyle.light;
+  headingPluginStyle = headingPluginStyle.copyWith(
+    textStyle: (EditorState editorState, Node node) {
+      final headingToFontSize = {
+        'h1': _baseFontSize + 12,
+        'h2': _baseFontSize + 8,
+        'h3': _baseFontSize + 4,
+        'h4': _baseFontSize,
+        'h5': _baseFontSize,
+        'h6': _baseFontSize,
+      };
+      final fontSize =
+          headingToFontSize[node.attributes.heading] ?? _baseFontSize;
+      return TextStyle(fontSize: fontSize, fontWeight: FontWeight.w600);
+    },
+    padding: (EditorState editorState, Node node) {
+      final headingToPadding = {
+        'h1': basePadding + 6,
+        'h2': basePadding + 4,
+        'h3': basePadding + 2,
+        'h4': basePadding,
+        'h5': basePadding,
+        'h6': basePadding,
+      };
+      final padding = headingToPadding[node.attributes.heading] ?? basePadding;
+      return EdgeInsets.only(bottom: padding);
     },
     },
   );
   );
+  final pluginTheme =
+      theme.isDark ? darkPlguinStyleExtension : lightPlguinStyleExtension;
+  return pluginTheme.toList()
+    ..removeWhere((element) => element is HeadingPluginStyle)
+    ..add(headingPluginStyle);
 }
 }

+ 4 - 2
frontend/app_flowy/lib/plugins/doc/presentation/plugins/horizontal_rule_node_widget.dart

@@ -38,9 +38,11 @@ ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
 
 
 SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
 SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
   name: () => 'Horizontal rule',
   name: () => 'Horizontal rule',
-  icon: const Icon(
+  icon: (editorState, onSelected) => Icon(
     Icons.horizontal_rule,
     Icons.horizontal_rule,
-    color: Colors.black,
+    color: onSelected
+        ? editorState.editorStyle.selectionMenuItemSelectedIconColor
+        : editorState.editorStyle.selectionMenuItemIconColor,
     size: 18.0,
     size: 18.0,
   ),
   ),
   keywords: ['horizontal rule'],
   keywords: ['horizontal rule'],

+ 23 - 45
frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart

@@ -10,7 +10,6 @@ import 'package:flutter/services.dart';
 
 
 import 'package:file_picker/file_picker.dart';
 import 'package:file_picker/file_picker.dart';
 import 'package:flutter_localizations/flutter_localizations.dart';
 import 'package:flutter_localizations/flutter_localizations.dart';
-import 'package:google_fonts/google_fonts.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:universal_html/html.dart' as html;
 import 'package:universal_html/html.dart' as html;
 
 
@@ -38,7 +37,10 @@ class MyApp extends StatelessWidget {
       debugShowCheckedModeBanner: false,
       debugShowCheckedModeBanner: false,
       theme: ThemeData(
       theme: ThemeData(
         primarySwatch: Colors.blue,
         primarySwatch: Colors.blue,
+        // extensions: [HeadingPluginStyle.light],
       ),
       ),
+      darkTheme: ThemeData.dark(),
+      themeMode: ThemeMode.dark,
       home: const MyHomePage(title: 'AppFlowyEditor Example'),
       home: const MyHomePage(title: 'AppFlowyEditor Example'),
     );
     );
   }
   }
@@ -56,7 +58,6 @@ class _MyHomePageState extends State<MyHomePage> {
   int _pageIndex = 0;
   int _pageIndex = 0;
   EditorState? _editorState;
   EditorState? _editorState;
   bool darkMode = false;
   bool darkMode = false;
-  EditorStyle _editorStyle = EditorStyle.defaultStyle();
   Future<String>? _jsonString;
   Future<String>? _jsonString;
 
 
   @override
   @override
@@ -125,12 +126,31 @@ class _MyHomePageState extends State<MyHomePage> {
           _editorState!.transactionStream.listen((event) {
           _editorState!.transactionStream.listen((event) {
             debugPrint('Transaction: ${event.toJson()}');
             debugPrint('Transaction: ${event.toJson()}');
           });
           });
+          final themeData = darkMode
+              ? ThemeData.dark().copyWith(extensions: [
+                  HeadingPluginStyle.dark,
+                  CheckboxPluginStyle.dark,
+                  NumberListPluginStyle.dark,
+                  QuotedTextPluginStyle.dark,
+                  BulletedListPluginStyle.dark,
+                  EditorStyle.dark,
+                ])
+              : ThemeData.light().copyWith(
+                  extensions: [
+                    HeadingPluginStyle.light,
+                    CheckboxPluginStyle.light,
+                    NumberListPluginStyle.light,
+                    QuotedTextPluginStyle.light,
+                    BulletedListPluginStyle.light,
+                    EditorStyle.light,
+                  ],
+                );
           return Container(
           return Container(
             color: darkMode ? Colors.black : Colors.white,
             color: darkMode ? Colors.black : Colors.white,
             width: MediaQuery.of(context).size.width,
             width: MediaQuery.of(context).size.width,
             child: AppFlowyEditor(
             child: AppFlowyEditor(
               editorState: _editorState!,
               editorState: _editorState!,
-              editorStyle: _editorStyle,
+              themeData: themeData,
               editable: true,
               editable: true,
               customBuilders: {
               customBuilders: {
                 'text/code_block': CodeBlockNodeWidgetBuilder(),
                 'text/code_block': CodeBlockNodeWidgetBuilder(),
@@ -186,8 +206,6 @@ class _MyHomePageState extends State<MyHomePage> {
           icon: const Icon(Icons.color_lens),
           icon: const Icon(Icons.color_lens),
           onPressed: () {
           onPressed: () {
             setState(() {
             setState(() {
-              _editorStyle =
-                  darkMode ? EditorStyle.defaultStyle() : _customizedStyle();
               darkMode = !darkMode;
               darkMode = !darkMode;
             });
             });
           },
           },
@@ -256,44 +274,4 @@ class _MyHomePageState extends State<MyHomePage> {
       });
       });
     }
     }
   }
   }
-
-  EditorStyle _customizedStyle() {
-    final editorStyle = EditorStyle.defaultStyle();
-    return editorStyle.copyWith(
-      cursorColor: Colors.white,
-      selectionColor: Colors.blue.withOpacity(0.3),
-      textStyle: editorStyle.textStyle.copyWith(
-        defaultTextStyle: GoogleFonts.poppins().copyWith(
-          color: Colors.white,
-          fontSize: 14.0,
-        ),
-        defaultPlaceholderTextStyle: GoogleFonts.poppins().copyWith(
-          color: Colors.white.withOpacity(0.5),
-          fontSize: 14.0,
-        ),
-        bold: const TextStyle(fontWeight: FontWeight.w900),
-        code: TextStyle(
-          fontStyle: FontStyle.italic,
-          color: Colors.red[300],
-          backgroundColor: Colors.grey.withOpacity(0.3),
-        ),
-        highlightColorHex: '0x6FFFEB3B',
-      ),
-      pluginStyles: {
-        'text/quote': builtInPluginStyle
-          ..update(
-            'textStyle',
-            (_) {
-              return (EditorState editorState, Node node) {
-                return TextStyle(
-                  color: Colors.blue[200],
-                  fontStyle: FontStyle.italic,
-                  fontSize: 12.0,
-                );
-              };
-            },
-          ),
-      },
-    );
-  }
 }
 }

+ 2 - 2
frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart

@@ -46,7 +46,7 @@ ShortcutEventHandler _ignorekHandler = (editorState, event) {
 
 
 SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
 SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
   name: () => 'Code Block',
   name: () => 'Code Block',
-  icon: const Icon(
+  icon: (_, __) => const Icon(
     Icons.abc,
     Icons.abc,
     color: Colors.black,
     color: Colors.black,
     size: 18.0,
     size: 18.0,
@@ -167,7 +167,7 @@ class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge>
         textNode: widget.textNode,
         textNode: widget.textNode,
         editorState: widget.editorState,
         editorState: widget.editorState,
         textSpanDecorator: (textSpan) => TextSpan(
         textSpanDecorator: (textSpan) => TextSpan(
-          style: widget.editorState.editorStyle.textStyle.defaultTextStyle,
+          style: widget.editorState.editorStyle.textStyle,
           children: codeTextSpan,
           children: codeTextSpan,
         ),
         ),
       ),
       ),

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/horizontal_rule_node_widget.dart

@@ -38,7 +38,7 @@ ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
 
 
 SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
 SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
   name: () => 'Horizontal rule',
   name: () => 'Horizontal rule',
-  icon: const Icon(
+  icon: (_, __) => const Icon(
     Icons.horizontal_rule,
     Icons.horizontal_rule,
     color: Colors.black,
     color: Colors.black,
     size: 18.0,
     size: 18.0,

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart

@@ -6,7 +6,7 @@ import 'package:flutter_math_fork/flutter_math.dart';
 
 
 SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
 SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
   name: () => 'Tex',
   name: () => 'Tex',
-  icon: const Icon(
+  icon: (_, __) => const Icon(
     Icons.text_fields_rounded,
     Icons.text_fields_rounded,
     color: Colors.black,
     color: Colors.black,
     size: 18.0,
     size: 18.0,

+ 2 - 0
frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart

@@ -31,3 +31,5 @@ export 'src/render/rich_text/default_selectable.dart';
 export 'src/render/rich_text/flowy_rich_text.dart';
 export 'src/render/rich_text/flowy_rich_text.dart';
 export 'src/render/selection_menu/selection_menu_widget.dart';
 export 'src/render/selection_menu/selection_menu_widget.dart';
 export 'src/l10n/l10n.dart';
 export 'src/l10n/l10n.dart';
+export 'src/render/style/plugin_styles.dart';
+export 'src/render/style/editor_style.dart';

+ 4 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart

@@ -59,13 +59,14 @@ class EditorState {
   /// Stores the selection menu items.
   /// Stores the selection menu items.
   List<SelectionMenuItem> selectionMenuItems = [];
   List<SelectionMenuItem> selectionMenuItems = [];
 
 
-  /// Stores the editor style.
-  EditorStyle editorStyle = EditorStyle.defaultStyle();
-
   /// Operation stream.
   /// Operation stream.
   Stream<Transaction> get transactionStream => _observer.stream;
   Stream<Transaction> get transactionStream => _observer.stream;
   final StreamController<Transaction> _observer = StreamController.broadcast();
   final StreamController<Transaction> _observer = StreamController.broadcast();
 
 
+  late ThemeData themeData;
+  EditorStyle get editorStyle =>
+      themeData.extension<EditorStyle>() ?? EditorStyle.light;
+
   final UndoManager undoManager = UndoManager();
   final UndoManager undoManager = UndoManager();
   Selection? _cursorSelection;
   Selection? _cursorSelection;
 
 

+ 10 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/theme_extension.dart

@@ -0,0 +1,10 @@
+import 'package:flutter/material.dart';
+
+extension ThemeExtension on ThemeData {
+  T? extensionOrNull<T>() {
+    if (extensions.containsKey(T)) {
+      return extensions[T] as T;
+    }
+    return null;
+  }
+}

+ 9 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart

@@ -2,6 +2,7 @@ import 'package:appflowy_editor/src/core/document/node.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
 import 'package:appflowy_editor/src/infra/flowy_svg.dart';
 import 'package:appflowy_editor/src/infra/flowy_svg.dart';
 import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart';
 import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart';
+import 'package:appflowy_editor/src/render/style/editor_style.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 
 
 OverlayEntry? _imageUploadMenu;
 OverlayEntry? _imageUploadMenu;
@@ -20,6 +21,7 @@ void showImageUploadMenu(
       left: menuService.topLeft.dx,
       left: menuService.topLeft.dx,
       child: Material(
       child: Material(
         child: ImageUploadMenu(
         child: ImageUploadMenu(
+          editorState: editorState,
           onSubmitted: (text) {
           onSubmitted: (text) {
             // _dismissImageUploadMenu();
             // _dismissImageUploadMenu();
             editorState.insertImageNode(text);
             editorState.insertImageNode(text);
@@ -53,10 +55,12 @@ class ImageUploadMenu extends StatefulWidget {
     Key? key,
     Key? key,
     required this.onSubmitted,
     required this.onSubmitted,
     required this.onUpload,
     required this.onUpload,
+    this.editorState,
   }) : super(key: key);
   }) : super(key: key);
 
 
   final void Function(String text) onSubmitted;
   final void Function(String text) onSubmitted;
   final void Function(String text) onUpload;
   final void Function(String text) onUpload;
+  final EditorState? editorState;
 
 
   @override
   @override
   State<ImageUploadMenu> createState() => _ImageUploadMenuState();
   State<ImageUploadMenu> createState() => _ImageUploadMenuState();
@@ -66,6 +70,8 @@ class _ImageUploadMenuState extends State<ImageUploadMenu> {
   final _textEditingController = TextEditingController();
   final _textEditingController = TextEditingController();
   final _focusNode = FocusNode();
   final _focusNode = FocusNode();
 
 
+  EditorStyle? get style => widget.editorState?.editorStyle;
+
   @override
   @override
   void initState() {
   void initState() {
     super.initState();
     super.initState();
@@ -84,7 +90,7 @@ class _ImageUploadMenuState extends State<ImageUploadMenu> {
       width: 300,
       width: 300,
       padding: const EdgeInsets.all(24.0),
       padding: const EdgeInsets.all(24.0),
       decoration: BoxDecoration(
       decoration: BoxDecoration(
-        color: Colors.white,
+        color: style?.selectionMenuBackgroundColor ?? Colors.white,
         boxShadow: [
         boxShadow: [
           BoxShadow(
           BoxShadow(
             blurRadius: 5,
             blurRadius: 5,
@@ -108,12 +114,12 @@ class _ImageUploadMenuState extends State<ImageUploadMenu> {
   }
   }
 
 
   Widget _buildHeader(BuildContext context) {
   Widget _buildHeader(BuildContext context) {
-    return const Text(
+    return Text(
       'URL Image',
       'URL Image',
       textAlign: TextAlign.left,
       textAlign: TextAlign.left,
       style: TextStyle(
       style: TextStyle(
         fontSize: 14.0,
         fontSize: 14.0,
-        color: Colors.black,
+        color: style?.selectionMenuItemTextColor ?? Colors.black,
         fontWeight: FontWeight.w500,
         fontWeight: FontWeight.w500,
       ),
       ),
     );
     );

+ 12 - 4
frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart

@@ -1,10 +1,13 @@
+import 'package:appflowy_editor/src/editor_state.dart';
 import 'package:appflowy_editor/src/infra/flowy_svg.dart';
 import 'package:appflowy_editor/src/infra/flowy_svg.dart';
+import 'package:appflowy_editor/src/render/style/editor_style.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 
 
 class LinkMenu extends StatefulWidget {
 class LinkMenu extends StatefulWidget {
   const LinkMenu({
   const LinkMenu({
     Key? key,
     Key? key,
     this.linkText,
     this.linkText,
+    this.editorState,
     required this.onSubmitted,
     required this.onSubmitted,
     required this.onOpenLink,
     required this.onOpenLink,
     required this.onCopyLink,
     required this.onCopyLink,
@@ -13,6 +16,7 @@ class LinkMenu extends StatefulWidget {
   }) : super(key: key);
   }) : super(key: key);
 
 
   final String? linkText;
   final String? linkText;
+  final EditorState? editorState;
   final void Function(String text) onSubmitted;
   final void Function(String text) onSubmitted;
   final VoidCallback onOpenLink;
   final VoidCallback onOpenLink;
   final VoidCallback onCopyLink;
   final VoidCallback onCopyLink;
@@ -27,6 +31,8 @@ class _LinkMenuState extends State<LinkMenu> {
   final _textEditingController = TextEditingController();
   final _textEditingController = TextEditingController();
   final _focusNode = FocusNode();
   final _focusNode = FocusNode();
 
 
+  EditorStyle? get style => widget.editorState?.editorStyle;
+
   @override
   @override
   void initState() {
   void initState() {
     super.initState();
     super.initState();
@@ -48,7 +54,7 @@ class _LinkMenuState extends State<LinkMenu> {
       width: 350,
       width: 350,
       child: Container(
       child: Container(
         decoration: BoxDecoration(
         decoration: BoxDecoration(
-          color: Colors.white,
+          color: style?.selectionMenuBackgroundColor ?? Colors.white,
           boxShadow: [
           boxShadow: [
             BoxShadow(
             BoxShadow(
               blurRadius: 5,
               blurRadius: 5,
@@ -71,17 +77,19 @@ class _LinkMenuState extends State<LinkMenu> {
               if (widget.linkText != null) ...[
               if (widget.linkText != null) ...[
                 _buildIconButton(
                 _buildIconButton(
                   iconName: 'link',
                   iconName: 'link',
+                  color: style?.selectionMenuItemIconColor,
                   text: 'Open link',
                   text: 'Open link',
                   onPressed: widget.onOpenLink,
                   onPressed: widget.onOpenLink,
                 ),
                 ),
                 _buildIconButton(
                 _buildIconButton(
                   iconName: 'copy',
                   iconName: 'copy',
-                  color: Colors.black,
+                  color: style?.selectionMenuItemIconColor,
                   text: 'Copy link',
                   text: 'Copy link',
                   onPressed: widget.onCopyLink,
                   onPressed: widget.onCopyLink,
                 ),
                 ),
                 _buildIconButton(
                 _buildIconButton(
                   iconName: 'delete',
                   iconName: 'delete',
+                  color: style?.selectionMenuItemIconColor,
                   text: 'Remove link',
                   text: 'Remove link',
                   onPressed: widget.onRemoveLink,
                   onPressed: widget.onRemoveLink,
                 ),
                 ),
@@ -154,8 +162,8 @@ class _LinkMenuState extends State<LinkMenu> {
       label: Text(
       label: Text(
         text,
         text,
         textAlign: TextAlign.left,
         textAlign: TextAlign.left,
-        style: const TextStyle(
-          color: Colors.black,
+        style: TextStyle(
+          color: style?.selectionMenuItemTextColor ?? Colors.black,
           fontSize: 14.0,
           fontSize: 14.0,
         ),
         ),
       ),
       ),

+ 0 - 50
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/built_in_text_widget.dart

@@ -10,56 +10,6 @@ abstract class BuiltInTextWidget extends StatefulWidget {
   TextNode get textNode;
   TextNode get textNode;
 }
 }
 
 
-mixin BuiltInStyleMixin<T extends BuiltInTextWidget> on State<T> {
-  EdgeInsets get padding {
-    final padding = widget.editorState.editorStyle.style(
-      widget.editorState,
-      widget.textNode,
-      'padding',
-    );
-    if (padding is EdgeInsets) {
-      return padding;
-    }
-    return const EdgeInsets.all(0);
-  }
-
-  TextStyle get textStyle {
-    final textStyle = widget.editorState.editorStyle.style(
-      widget.editorState,
-      widget.textNode,
-      'textStyle',
-    );
-    if (textStyle is TextStyle) {
-      return textStyle;
-    }
-    return const TextStyle();
-  }
-
-  Size? get iconSize {
-    final iconSize = widget.editorState.editorStyle.style(
-      widget.editorState,
-      widget.textNode,
-      'iconSize',
-    );
-    if (iconSize is Size) {
-      return iconSize;
-    }
-    return const Size.square(18.0);
-  }
-
-  EdgeInsets? get iconPadding {
-    final iconPadding = widget.editorState.editorStyle.style(
-      widget.editorState,
-      widget.textNode,
-      'iconPadding',
-    );
-    if (iconPadding is EdgeInsets) {
-      return iconPadding;
-    }
-    return const EdgeInsets.all(0);
-  }
-}
-
 mixin BuiltInTextWidgetMixin<T extends BuiltInTextWidget> on State<T>
 mixin BuiltInTextWidgetMixin<T extends BuiltInTextWidget> on State<T>
     implements DefaultSelectable {
     implements DefaultSelectable {
   @override
   @override

+ 25 - 12
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart

@@ -1,13 +1,14 @@
 import 'package:appflowy_editor/src/core/document/node.dart';
 import 'package:appflowy_editor/src/core/document/node.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
-import 'package:appflowy_editor/src/infra/flowy_svg.dart';
 import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
 import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
 import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
 import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
 import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
 import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
 import 'package:appflowy_editor/src/render/selection/selectable.dart';
 import 'package:appflowy_editor/src/render/selection/selectable.dart';
+import 'package:appflowy_editor/src/render/style/plugin_styles.dart';
 import 'package:appflowy_editor/src/service/render_plugin_service.dart';
 import 'package:appflowy_editor/src/service/render_plugin_service.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
 import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
+import 'package:appflowy_editor/src/extensions/theme_extension.dart';
 
 
 class BulletedListTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
 class BulletedListTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
   @override
   @override
@@ -45,11 +46,7 @@ class BulletedListTextNodeWidget extends BuiltInTextWidget {
 // customize
 // customize
 
 
 class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
 class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
-    with
-        SelectableMixin,
-        DefaultSelectable,
-        BuiltInStyleMixin,
-        BuiltInTextWidgetMixin {
+    with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
   @override
   @override
   final iconKey = GlobalKey();
   final iconKey = GlobalKey();
 
 
@@ -64,6 +61,25 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
     return super.baseOffset.translate(0, padding.top);
     return super.baseOffset.translate(0, padding.top);
   }
   }
 
 
+  BulletedListPluginStyle get style =>
+      Theme.of(context).extensionOrNull<BulletedListPluginStyle>() ??
+      BulletedListPluginStyle.light;
+
+  EdgeInsets get padding => style.padding(
+        widget.editorState,
+        widget.textNode,
+      );
+
+  TextStyle get textStyle => style.textStyle(
+        widget.editorState,
+        widget.textNode,
+      );
+
+  Widget get icon => style.icon(
+        widget.editorState,
+        widget.textNode,
+      );
+
   @override
   @override
   Widget buildWithSingle(BuildContext context) {
   Widget buildWithSingle(BuildContext context) {
     return Padding(
     return Padding(
@@ -71,12 +87,9 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
       child: Row(
       child: Row(
         crossAxisAlignment: CrossAxisAlignment.start,
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
         children: [
-          FlowySvg(
+          Container(
             key: iconKey,
             key: iconKey,
-            width: iconSize?.width,
-            height: iconSize?.height,
-            padding: iconPadding,
-            name: 'point',
+            child: icon,
           ),
           ),
           Flexible(
           Flexible(
             child: FlowyRichText(
             child: FlowyRichText(
@@ -86,7 +99,7 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
                   textSpan.updateTextStyle(textStyle),
                   textSpan.updateTextStyle(textStyle),
               placeholderTextSpanDecorator: (textSpan) =>
               placeholderTextSpanDecorator: (textSpan) =>
                   textSpan.updateTextStyle(textStyle),
                   textSpan.updateTextStyle(textStyle),
-              lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
+              lineHeight: widget.editorState.editorStyle.lineHeight,
               textNode: widget.textNode,
               textNode: widget.textNode,
               editorState: widget.editorState,
               editorState: widget.editorState,
             ),
             ),

+ 23 - 13
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart

@@ -1,10 +1,10 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/src/commands/text/text_commands.dart';
 import 'package:appflowy_editor/src/commands/text/text_commands.dart';
-import 'package:appflowy_editor/src/infra/flowy_svg.dart';
 import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
 import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
 
 
 import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
 import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
+import 'package:appflowy_editor/src/extensions/theme_extension.dart';
 
 
 class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
 class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
   @override
   @override
@@ -39,11 +39,7 @@ class CheckboxNodeWidget extends BuiltInTextWidget {
 }
 }
 
 
 class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
 class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
-    with
-        SelectableMixin,
-        DefaultSelectable,
-        BuiltInStyleMixin,
-        BuiltInTextWidgetMixin {
+    with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
   @override
   @override
   final iconKey = GlobalKey();
   final iconKey = GlobalKey();
 
 
@@ -58,6 +54,25 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
     return super.baseOffset.translate(0, padding.top);
     return super.baseOffset.translate(0, padding.top);
   }
   }
 
 
+  CheckboxPluginStyle get style =>
+      Theme.of(context).extensionOrNull<CheckboxPluginStyle>() ??
+      CheckboxPluginStyle.light;
+
+  EdgeInsets get padding => style.padding(
+        widget.editorState,
+        widget.textNode,
+      );
+
+  TextStyle get textStyle => style.textStyle(
+        widget.editorState,
+        widget.textNode,
+      );
+
+  Widget get icon => style.icon(
+        widget.editorState,
+        widget.textNode,
+      );
+
   @override
   @override
   Widget buildWithSingle(BuildContext context) {
   Widget buildWithSingle(BuildContext context) {
     final check = widget.textNode.attributes.check;
     final check = widget.textNode.attributes.check;
@@ -68,12 +83,7 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
         children: [
         children: [
           GestureDetector(
           GestureDetector(
             key: iconKey,
             key: iconKey,
-            child: FlowySvg(
-              width: iconSize?.width,
-              height: iconSize?.height,
-              padding: iconPadding,
-              name: check ? 'check' : 'uncheck',
-            ),
+            child: icon,
             onTap: () async {
             onTap: () async {
               await widget.editorState.formatTextToCheckbox(
               await widget.editorState.formatTextToCheckbox(
                 widget.editorState,
                 widget.editorState,
@@ -86,7 +96,7 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
             child: FlowyRichText(
             child: FlowyRichText(
               key: _richTextKey,
               key: _richTextKey,
               placeholderText: 'To-do',
               placeholderText: 'To-do',
-              lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
+              lineHeight: widget.editorState.editorStyle.lineHeight,
               textNode: widget.textNode,
               textNode: widget.textNode,
               textSpanDecorator: (textSpan) =>
               textSpanDecorator: (textSpan) =>
                   textSpan.updateTextStyle(textStyle),
                   textSpan.updateTextStyle(textStyle),

+ 5 - 4
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart

@@ -202,12 +202,13 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
   }
   }
 
 
   TextSpan get _placeholderTextSpan {
   TextSpan get _placeholderTextSpan {
-    final style = widget.editorState.editorStyle.textStyle;
+    final placeholderTextStyle =
+        widget.editorState.editorStyle.placeholderTextStyle;
     return TextSpan(
     return TextSpan(
       children: [
       children: [
         TextSpan(
         TextSpan(
           text: widget.placeholderText,
           text: widget.placeholderText,
-          style: style.defaultPlaceholderTextStyle,
+          style: placeholderTextStyle,
         ),
         ),
       ],
       ],
     );
     );
@@ -216,10 +217,10 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
   TextSpan get _textSpan {
   TextSpan get _textSpan {
     var offset = 0;
     var offset = 0;
     List<TextSpan> textSpans = [];
     List<TextSpan> textSpans = [];
-    final style = widget.editorState.editorStyle.textStyle;
+    final style = widget.editorState.editorStyle;
     final textInserts = widget.textNode.delta.whereType<TextInsert>();
     final textInserts = widget.textNode.delta.whereType<TextInsert>();
     for (final textInsert in textInserts) {
     for (final textInsert in textInserts) {
-      var textStyle = style.defaultTextStyle;
+      var textStyle = style.textStyle!;
       GestureRecognizer? recognizer;
       GestureRecognizer? recognizer;
       final attributes = textInsert.attributes;
       final attributes = textInsert.attributes;
       if (attributes != null) {
       if (attributes != null) {

+ 18 - 2
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart

@@ -4,10 +4,12 @@ import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
 import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
 import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
 import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
 import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
 import 'package:appflowy_editor/src/render/selection/selectable.dart';
 import 'package:appflowy_editor/src/render/selection/selectable.dart';
+import 'package:appflowy_editor/src/render/style/plugin_styles.dart';
 import 'package:appflowy_editor/src/service/render_plugin_service.dart';
 import 'package:appflowy_editor/src/service/render_plugin_service.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
 import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
 import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
 import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
+import 'package:appflowy_editor/src/extensions/theme_extension.dart';
 
 
 class HeadingTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
 class HeadingTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
   @override
   @override
@@ -43,7 +45,7 @@ class HeadingTextNodeWidget extends BuiltInTextWidget {
 
 
 // customize
 // customize
 class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
 class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
-    with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
+    with SelectableMixin, DefaultSelectable {
   @override
   @override
   GlobalKey? get iconKey => null;
   GlobalKey? get iconKey => null;
 
 
@@ -58,6 +60,20 @@ class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
     return padding.topLeft;
     return padding.topLeft;
   }
   }
 
 
+  HeadingPluginStyle get style =>
+      Theme.of(context).extensionOrNull<HeadingPluginStyle>() ??
+      HeadingPluginStyle.light;
+
+  EdgeInsets get padding => style.padding(
+        widget.editorState,
+        widget.textNode,
+      );
+
+  TextStyle get textStyle => style.textStyle(
+        widget.editorState,
+        widget.textNode,
+      );
+
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     return Padding(
     return Padding(
@@ -68,7 +84,7 @@ class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
         placeholderTextSpanDecorator: (textSpan) =>
         placeholderTextSpanDecorator: (textSpan) =>
             textSpan.updateTextStyle(textStyle),
             textSpan.updateTextStyle(textStyle),
         textSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle),
         textSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle),
-        lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
+        lineHeight: widget.editorState.editorStyle.lineHeight,
         textNode: widget.textNode,
         textNode: widget.textNode,
         editorState: widget.editorState,
         editorState: widget.editorState,
       ),
       ),

+ 24 - 8
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart

@@ -4,10 +4,12 @@ import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
 import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
 import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
 import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
 import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
 import 'package:appflowy_editor/src/render/selection/selectable.dart';
 import 'package:appflowy_editor/src/render/selection/selectable.dart';
+import 'package:appflowy_editor/src/render/style/plugin_styles.dart';
 import 'package:appflowy_editor/src/service/render_plugin_service.dart';
 import 'package:appflowy_editor/src/service/render_plugin_service.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
 import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
 import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
 import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
+import 'package:appflowy_editor/src/extensions/theme_extension.dart';
 
 
 class NumberListTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
 class NumberListTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
   @override
   @override
@@ -43,7 +45,7 @@ class NumberListTextNodeWidget extends BuiltInTextWidget {
 }
 }
 
 
 class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
 class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
-    with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
+    with SelectableMixin, DefaultSelectable {
   @override
   @override
   final iconKey = GlobalKey();
   final iconKey = GlobalKey();
 
 
@@ -58,6 +60,25 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
     return super.baseOffset.translate(0, padding.top);
     return super.baseOffset.translate(0, padding.top);
   }
   }
 
 
+  NumberListPluginStyle get style =>
+      Theme.of(context).extensionOrNull<NumberListPluginStyle>() ??
+      NumberListPluginStyle.light;
+
+  EdgeInsets get padding => style.padding(
+        widget.editorState,
+        widget.textNode,
+      );
+
+  TextStyle get textStyle => style.textStyle(
+        widget.editorState,
+        widget.textNode,
+      );
+
+  Widget get icon => style.icon(
+        widget.editorState,
+        widget.textNode,
+      );
+
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     return Padding(
     return Padding(
@@ -67,12 +88,7 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
         children: [
         children: [
           Container(
           Container(
             key: iconKey,
             key: iconKey,
-            padding: iconPadding,
-            child: Text(
-              '${widget.textNode.attributes.number.toString()}.',
-              // FIXME: customize
-              style: const TextStyle(fontSize: 16.0, color: Colors.black),
-            ),
+            child: icon,
           ),
           ),
           Flexible(
           Flexible(
             child: FlowyRichText(
             child: FlowyRichText(
@@ -80,7 +96,7 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
               placeholderText: 'List',
               placeholderText: 'List',
               textNode: widget.textNode,
               textNode: widget.textNode,
               editorState: widget.editorState,
               editorState: widget.editorState,
-              lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
+              lineHeight: widget.editorState.editorStyle.lineHeight,
               placeholderTextSpanDecorator: (textSpan) =>
               placeholderTextSpanDecorator: (textSpan) =>
                   textSpan.updateTextStyle(textStyle),
                   textSpan.updateTextStyle(textStyle),
               textSpanDecorator: (textSpan) =>
               textSpanDecorator: (textSpan) =>

+ 25 - 7
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart

@@ -1,13 +1,14 @@
 import 'package:appflowy_editor/src/core/document/node.dart';
 import 'package:appflowy_editor/src/core/document/node.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
-import 'package:appflowy_editor/src/infra/flowy_svg.dart';
 import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
 import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
 import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
 import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
 import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
 import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
 import 'package:appflowy_editor/src/render/selection/selectable.dart';
 import 'package:appflowy_editor/src/render/selection/selectable.dart';
+import 'package:appflowy_editor/src/render/style/plugin_styles.dart';
 import 'package:appflowy_editor/src/service/render_plugin_service.dart';
 import 'package:appflowy_editor/src/service/render_plugin_service.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
 import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
+import 'package:appflowy_editor/src/extensions/theme_extension.dart';
 
 
 class QuotedTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
 class QuotedTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
   @override
   @override
@@ -44,7 +45,7 @@ class QuotedTextNodeWidget extends BuiltInTextWidget {
 // customize
 // customize
 
 
 class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
 class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
-    with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
+    with SelectableMixin, DefaultSelectable {
   @override
   @override
   final iconKey = GlobalKey();
   final iconKey = GlobalKey();
 
 
@@ -59,6 +60,25 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
     return super.baseOffset.translate(0, padding.top);
     return super.baseOffset.translate(0, padding.top);
   }
   }
 
 
+  QuotedTextPluginStyle get style =>
+      Theme.of(context).extensionOrNull<QuotedTextPluginStyle>() ??
+      QuotedTextPluginStyle.light;
+
+  EdgeInsets get padding => style.padding(
+        widget.editorState,
+        widget.textNode,
+      );
+
+  TextStyle get textStyle => style.textStyle(
+        widget.editorState,
+        widget.textNode,
+      );
+
+  Widget get icon => style.icon(
+        widget.editorState,
+        widget.textNode,
+      );
+
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     return Padding(
     return Padding(
@@ -67,11 +87,9 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
         child: Row(
         child: Row(
           crossAxisAlignment: CrossAxisAlignment.stretch,
           crossAxisAlignment: CrossAxisAlignment.stretch,
           children: [
           children: [
-            FlowySvg(
+            Container(
               key: iconKey,
               key: iconKey,
-              width: iconSize?.width,
-              padding: iconPadding,
-              name: 'quote',
+              child: icon,
             ),
             ),
             Flexible(
             Flexible(
               child: FlowyRichText(
               child: FlowyRichText(
@@ -82,7 +100,7 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
                     textSpan.updateTextStyle(textStyle),
                     textSpan.updateTextStyle(textStyle),
                 placeholderTextSpanDecorator: (textSpan) =>
                 placeholderTextSpanDecorator: (textSpan) =>
                     textSpan.updateTextStyle(textStyle),
                     textSpan.updateTextStyle(textStyle),
-                lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
+                lineHeight: widget.editorState.editorStyle.lineHeight,
                 editorState: widget.editorState,
                 editorState: widget.editorState,
               ),
               ),
             ),
             ),

+ 11 - 8
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart

@@ -4,6 +4,7 @@ import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
 import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
 import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
 import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
 import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
 import 'package:appflowy_editor/src/render/selection/selectable.dart';
 import 'package:appflowy_editor/src/render/selection/selectable.dart';
+import 'package:appflowy_editor/src/render/style/editor_style.dart';
 import 'package:appflowy_editor/src/service/render_plugin_service.dart';
 import 'package:appflowy_editor/src/service/render_plugin_service.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
 import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
@@ -43,11 +44,7 @@ class RichTextNodeWidget extends BuiltInTextWidget {
 // customize
 // customize
 
 
 class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
 class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
-    with
-        SelectableMixin,
-        DefaultSelectable,
-        BuiltInStyleMixin,
-        BuiltInTextWidgetMixin {
+    with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
   @override
   @override
   GlobalKey? get iconKey => null;
   GlobalKey? get iconKey => null;
 
 
@@ -59,20 +56,26 @@ class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
 
 
   @override
   @override
   Offset get baseOffset {
   Offset get baseOffset {
-    return padding.topLeft;
+    return textPadding.topLeft;
   }
   }
 
 
+  EditorStyle get style => widget.editorState.editorStyle;
+
+  EdgeInsets get textPadding => style.textPadding!;
+
+  TextStyle get textStyle => style.textStyle!;
+
   @override
   @override
   Widget buildWithSingle(BuildContext context) {
   Widget buildWithSingle(BuildContext context) {
     return Padding(
     return Padding(
-      padding: padding,
+      padding: textPadding,
       child: FlowyRichText(
       child: FlowyRichText(
         key: _richTextKey,
         key: _richTextKey,
         textNode: widget.textNode,
         textNode: widget.textNode,
         textSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle),
         textSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle),
         placeholderTextSpanDecorator: (textSpan) =>
         placeholderTextSpanDecorator: (textSpan) =>
             textSpan.updateTextStyle(textStyle),
             textSpan.updateTextStyle(textStyle),
-        lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
+        lineHeight: widget.editorState.editorStyle.lineHeight,
         editorState: widget.editorState,
         editorState: widget.editorState,
       ),
       ),
     );
     );

+ 31 - 13
frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart

@@ -3,7 +3,7 @@ import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service
 import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
 import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 
 
-class SelectionMenuItemWidget extends StatelessWidget {
+class SelectionMenuItemWidget extends StatefulWidget {
   const SelectionMenuItemWidget({
   const SelectionMenuItemWidget({
     Key? key,
     Key? key,
     required this.editorState,
     required this.editorState,
@@ -11,7 +11,6 @@ class SelectionMenuItemWidget extends StatelessWidget {
     required this.item,
     required this.item,
     required this.isSelected,
     required this.isSelected,
     this.width = 140.0,
     this.width = 140.0,
-    this.selectedColor = const Color(0xFFE0F8FF),
   }) : super(key: key);
   }) : super(key: key);
 
 
   final EditorState editorState;
   final EditorState editorState;
@@ -19,33 +18,52 @@ class SelectionMenuItemWidget extends StatelessWidget {
   final SelectionMenuItem item;
   final SelectionMenuItem item;
   final double width;
   final double width;
   final bool isSelected;
   final bool isSelected;
-  final Color selectedColor;
+
+  @override
+  State<SelectionMenuItemWidget> createState() =>
+      _SelectionMenuItemWidgetState();
+}
+
+class _SelectionMenuItemWidgetState extends State<SelectionMenuItemWidget> {
+  var _onHover = false;
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
+    final editorStyle = widget.editorState.editorStyle;
     return Container(
     return Container(
       padding: const EdgeInsets.fromLTRB(8.0, 5.0, 8.0, 5.0),
       padding: const EdgeInsets.fromLTRB(8.0, 5.0, 8.0, 5.0),
       child: SizedBox(
       child: SizedBox(
-        width: width,
+        width: widget.width,
         child: TextButton.icon(
         child: TextButton.icon(
-          icon: item.icon,
+          icon: widget.item
+              .icon(widget.editorState, widget.isSelected || _onHover),
           style: ButtonStyle(
           style: ButtonStyle(
             alignment: Alignment.centerLeft,
             alignment: Alignment.centerLeft,
-            overlayColor: MaterialStateProperty.all(selectedColor),
-            backgroundColor: isSelected
-                ? MaterialStateProperty.all(selectedColor)
+            overlayColor: MaterialStateProperty.all(
+                editorStyle.selectionMenuItemSelectedColor),
+            backgroundColor: widget.isSelected
+                ? MaterialStateProperty.all(
+                    editorStyle.selectionMenuItemSelectedColor)
                 : MaterialStateProperty.all(Colors.transparent),
                 : MaterialStateProperty.all(Colors.transparent),
           ),
           ),
           label: Text(
           label: Text(
-            item.name(),
+            widget.item.name(),
             textAlign: TextAlign.left,
             textAlign: TextAlign.left,
-            style: const TextStyle(
-              color: Colors.black,
-              fontSize: 14.0,
+            style: TextStyle(
+              color: (widget.isSelected || _onHover)
+                  ? editorStyle.selectionMenuItemSelectedTextColor
+                  : editorStyle.selectionMenuItemTextColor,
+              fontSize: 12.0,
             ),
             ),
           ),
           ),
           onPressed: () {
           onPressed: () {
-            item.handler(editorState, menuService, context);
+            widget.item
+                .handler(widget.editorState, widget.menuService, context);
+          },
+          onHover: (value) {
+            setState(() {
+              _onHover = value;
+            });
           },
           },
         ),
         ),
       ),
       ),

+ 38 - 18
frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart

@@ -61,19 +61,27 @@ class SelectionMenu implements SelectionMenuService {
     //  Just subtract the padding here as a result.
     //  Just subtract the padding here as a result.
     const menuHeight = 200.0;
     const menuHeight = 200.0;
     const menuOffset = Offset(10, 10);
     const menuOffset = Offset(10, 10);
-    final baseOffset =
+    final editorOffset =
         editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
         editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
-    var offset = selectionRects.first.bottomRight + menuOffset;
-    if (offset.dy >=
-        baseOffset.dy + editorState.renderBox!.size.height - menuHeight) {
-      offset = selectionRects.first.topRight - menuOffset;
-      offset = offset.translate(0, -menuHeight);
+    final editorHeight = editorState.renderBox!.size.height;
+
+    // show below defualt
+    var showBelow = true;
+    final bottomRight = selectionRects.first.bottomRight;
+    final topRight = selectionRects.first.topRight;
+    var offset = bottomRight + menuOffset;
+    // overflow
+    if (offset.dy + menuHeight >= editorOffset.dy + editorHeight) {
+      // show above
+      offset = topRight - menuOffset;
+      showBelow = false;
     }
     }
     _topLeft = offset;
     _topLeft = offset;
 
 
     _selectionMenuEntry = OverlayEntry(builder: (context) {
     _selectionMenuEntry = OverlayEntry(builder: (context) {
       return Positioned(
       return Positioned(
-        top: offset.dy,
+        top: showBelow ? offset.dy : null,
+        bottom: showBelow ? null : editorHeight - offset.dy,
         left: offset.dx,
         left: offset.dx,
         child: SelectionMenuWidget(
         child: SelectionMenuWidget(
           items: [
           items: [
@@ -131,7 +139,8 @@ List<SelectionMenuItem> get defaultSelectionMenuItems =>
 final List<SelectionMenuItem> _defaultSelectionMenuItems = [
 final List<SelectionMenuItem> _defaultSelectionMenuItems = [
   SelectionMenuItem(
   SelectionMenuItem(
     name: () => AppFlowyEditorLocalizations.current.text,
     name: () => AppFlowyEditorLocalizations.current.text,
-    icon: _selectionMenuIcon('text'),
+    icon: (editorState, onSelected) =>
+        _selectionMenuIcon('text', editorState, onSelected),
     keywords: ['text'],
     keywords: ['text'],
     handler: (editorState, _, __) {
     handler: (editorState, _, __) {
       insertTextNodeAfterSelection(editorState, {});
       insertTextNodeAfterSelection(editorState, {});
@@ -139,7 +148,8 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
   ),
   ),
   SelectionMenuItem(
   SelectionMenuItem(
     name: () => AppFlowyEditorLocalizations.current.heading1,
     name: () => AppFlowyEditorLocalizations.current.heading1,
-    icon: _selectionMenuIcon('h1'),
+    icon: (editorState, onSelected) =>
+        _selectionMenuIcon('h1', editorState, onSelected),
     keywords: ['heading 1, h1'],
     keywords: ['heading 1, h1'],
     handler: (editorState, _, __) {
     handler: (editorState, _, __) {
       insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h1);
       insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h1);
@@ -147,7 +157,8 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
   ),
   ),
   SelectionMenuItem(
   SelectionMenuItem(
     name: () => AppFlowyEditorLocalizations.current.heading2,
     name: () => AppFlowyEditorLocalizations.current.heading2,
-    icon: _selectionMenuIcon('h2'),
+    icon: (editorState, onSelected) =>
+        _selectionMenuIcon('h2', editorState, onSelected),
     keywords: ['heading 2, h2'],
     keywords: ['heading 2, h2'],
     handler: (editorState, _, __) {
     handler: (editorState, _, __) {
       insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h2);
       insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h2);
@@ -155,7 +166,8 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
   ),
   ),
   SelectionMenuItem(
   SelectionMenuItem(
     name: () => AppFlowyEditorLocalizations.current.heading3,
     name: () => AppFlowyEditorLocalizations.current.heading3,
-    icon: _selectionMenuIcon('h3'),
+    icon: (editorState, onSelected) =>
+        _selectionMenuIcon('h3', editorState, onSelected),
     keywords: ['heading 3, h3'],
     keywords: ['heading 3, h3'],
     handler: (editorState, _, __) {
     handler: (editorState, _, __) {
       insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h3);
       insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h3);
@@ -163,13 +175,15 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
   ),
   ),
   SelectionMenuItem(
   SelectionMenuItem(
     name: () => AppFlowyEditorLocalizations.current.image,
     name: () => AppFlowyEditorLocalizations.current.image,
-    icon: _selectionMenuIcon('image'),
+    icon: (editorState, onSelected) =>
+        _selectionMenuIcon('image', editorState, onSelected),
     keywords: ['image'],
     keywords: ['image'],
     handler: showImageUploadMenu,
     handler: showImageUploadMenu,
   ),
   ),
   SelectionMenuItem(
   SelectionMenuItem(
     name: () => AppFlowyEditorLocalizations.current.bulletedList,
     name: () => AppFlowyEditorLocalizations.current.bulletedList,
-    icon: _selectionMenuIcon('bulleted_list'),
+    icon: (editorState, onSelected) =>
+        _selectionMenuIcon('bulleted_list', editorState, onSelected),
     keywords: ['bulleted list', 'list', 'unordered list'],
     keywords: ['bulleted list', 'list', 'unordered list'],
     handler: (editorState, _, __) {
     handler: (editorState, _, __) {
       insertBulletedListAfterSelection(editorState);
       insertBulletedListAfterSelection(editorState);
@@ -177,7 +191,8 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
   ),
   ),
   SelectionMenuItem(
   SelectionMenuItem(
     name: () => AppFlowyEditorLocalizations.current.numberedList,
     name: () => AppFlowyEditorLocalizations.current.numberedList,
-    icon: _selectionMenuIcon('number'),
+    icon: (editorState, onSelected) =>
+        _selectionMenuIcon('number', editorState, onSelected),
     keywords: ['numbered list', 'list', 'ordered list'],
     keywords: ['numbered list', 'list', 'ordered list'],
     handler: (editorState, _, __) {
     handler: (editorState, _, __) {
       insertNumberedListAfterSelection(editorState);
       insertNumberedListAfterSelection(editorState);
@@ -185,7 +200,8 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
   ),
   ),
   SelectionMenuItem(
   SelectionMenuItem(
     name: () => AppFlowyEditorLocalizations.current.checkbox,
     name: () => AppFlowyEditorLocalizations.current.checkbox,
-    icon: _selectionMenuIcon('checkbox'),
+    icon: (editorState, onSelected) =>
+        _selectionMenuIcon('checkbox', editorState, onSelected),
     keywords: ['todo list', 'list', 'checkbox list'],
     keywords: ['todo list', 'list', 'checkbox list'],
     handler: (editorState, _, __) {
     handler: (editorState, _, __) {
       insertCheckboxAfterSelection(editorState);
       insertCheckboxAfterSelection(editorState);
@@ -193,7 +209,8 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
   ),
   ),
   SelectionMenuItem(
   SelectionMenuItem(
     name: () => AppFlowyEditorLocalizations.current.quote,
     name: () => AppFlowyEditorLocalizations.current.quote,
-    icon: _selectionMenuIcon('quote'),
+    icon: (editorState, onSelected) =>
+        _selectionMenuIcon('quote', editorState, onSelected),
     keywords: ['quote', 'refer'],
     keywords: ['quote', 'refer'],
     handler: (editorState, _, __) {
     handler: (editorState, _, __) {
       insertQuoteAfterSelection(editorState);
       insertQuoteAfterSelection(editorState);
@@ -201,10 +218,13 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
   ),
   ),
 ];
 ];
 
 
-Widget _selectionMenuIcon(String name) {
+Widget _selectionMenuIcon(
+    String name, EditorState editorState, bool onSelected) {
   return FlowySvg(
   return FlowySvg(
     name: 'selection_menu/$name',
     name: 'selection_menu/$name',
-    color: Colors.black,
+    color: onSelected
+        ? editorState.editorStyle.selectionMenuItemSelectedIconColor
+        : editorState.editorStyle.selectionMenuItemIconColor,
     width: 18.0,
     width: 18.0,
     height: 18.0,
     height: 18.0,
   );
   );

+ 2 - 2
frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart

@@ -29,7 +29,7 @@ class SelectionMenuItem {
   }
   }
 
 
   final String Function() name;
   final String Function() name;
-  final Widget icon;
+  final Widget Function(EditorState editorState, bool onSelected) icon;
 
 
   /// Customizes keywords for item.
   /// Customizes keywords for item.
   ///
   ///
@@ -142,7 +142,7 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
       onKey: _onKey,
       onKey: _onKey,
       child: Container(
       child: Container(
         decoration: BoxDecoration(
         decoration: BoxDecoration(
-          color: Colors.white,
+          color: widget.editorState.editorStyle.selectionMenuBackgroundColor,
           boxShadow: [
           boxShadow: [
             BoxShadow(
             BoxShadow(
               blurRadius: 5,
               blurRadius: 5,

+ 159 - 214
frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart

@@ -1,202 +1,78 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 
 
-import 'package:appflowy_editor/src/core/document/node.dart';
-import 'package:appflowy_editor/src/editor_state.dart';
-import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
+Iterable<ThemeExtension<dynamic>> get lightEditorStyleExtension => [
+      EditorStyle.light,
+    ];
+
+Iterable<ThemeExtension<dynamic>> get darkEditorStyleExtension => [
+      EditorStyle.dark,
+    ];
+
+class EditorStyle extends ThemeExtension<EditorStyle> {
+  // Editor styles
+  final EdgeInsets? padding;
+  final Color? cursorColor;
+  final Color? selectionColor;
+
+  // Selection menu styles
+  final Color? selectionMenuBackgroundColor;
+  final Color? selectionMenuItemTextColor;
+  final Color? selectionMenuItemIconColor;
+  final Color? selectionMenuItemSelectedTextColor;
+  final Color? selectionMenuItemSelectedIconColor;
+  final Color? selectionMenuItemSelectedColor;
+
+  // Text styles
+  final EdgeInsets? textPadding;
+  final TextStyle? textStyle;
+  final TextStyle? placeholderTextStyle;
+  final double lineHeight;
 
 
-typedef PluginStyler = Object Function(EditorState editorState, Node node);
-typedef PluginStyle = Map<String, PluginStyler>;
+  // Rich text styles
+  final TextStyle? bold;
+  final TextStyle? italic;
+  final TextStyle? underline;
+  final TextStyle? strikethrough;
+  final TextStyle? href;
+  final TextStyle? code;
+  final String? highlightColorHex;
 
 
-/// Editor style configuration
-class EditorStyle {
   EditorStyle({
   EditorStyle({
     required this.padding,
     required this.padding,
-    required this.textStyle,
     required this.cursorColor,
     required this.cursorColor,
     required this.selectionColor,
     required this.selectionColor,
-    Map<String, PluginStyle> pluginStyles = const {},
-  }) {
-    _pluginStyles.addAll(pluginStyles);
-  }
-
-  EditorStyle.defaultStyle()
-      : padding = const EdgeInsets.fromLTRB(200.0, 0.0, 200.0, 0.0),
-        textStyle = BuiltInTextStyle.builtIn(),
-        cursorColor = const Color(0xFF00BCF0),
-        selectionColor = const Color.fromARGB(53, 111, 201, 231);
-
-  /// The margin of the document context from the editor.
-  final EdgeInsets padding;
-  final BuiltInTextStyle textStyle;
-  final Color cursorColor;
-  final Color selectionColor;
-
-  final Map<String, PluginStyle> _pluginStyles = Map.from(builtInTextStylers);
-
-  Object? style(EditorState editorState, Node node, String key) {
-    final styler = _pluginStyles[node.id]?[key];
-    if (styler != null) {
-      return styler(editorState, node);
-    }
-    return null;
-  }
-
-  EditorStyle copyWith({
-    EdgeInsets? padding,
-    BuiltInTextStyle? textStyle,
-    Color? cursorColor,
-    Color? selectionColor,
-    Map<String, PluginStyle>? pluginStyles,
-  }) {
-    return EditorStyle(
-      padding: padding ?? this.padding,
-      textStyle: textStyle ?? this.textStyle,
-      cursorColor: cursorColor ?? this.cursorColor,
-      selectionColor: selectionColor ?? this.selectionColor,
-      pluginStyles: pluginStyles ?? {},
-    );
-  }
-
-  @override
-  bool operator ==(Object other) {
-    if (identical(this, other)) return true;
-
-    return other is EditorStyle &&
-        other.padding == padding &&
-        other.textStyle == textStyle &&
-        other.cursorColor == cursorColor &&
-        other.selectionColor == selectionColor;
-  }
-
-  @override
-  int get hashCode {
-    return padding.hashCode ^
-        textStyle.hashCode ^
-        cursorColor.hashCode ^
-        selectionColor.hashCode;
-  }
-}
-
-PluginStyle get builtInPluginStyle => Map.from({
-      'padding': (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
-      'textStyle': (_, __) => const TextStyle(),
-      'iconSize': (_, __) => const Size.square(20.0),
-      'iconPadding': (_, __) => const EdgeInsets.only(right: 5.0),
-    });
-
-Map<String, PluginStyle> builtInTextStylers = {
-  'text': builtInPluginStyle,
-  'text/checkbox': builtInPluginStyle
-    ..update(
-      'textStyle',
-      (_) => (EditorState editorState, Node node) {
-        if (node is TextNode && node.attributes.check == true) {
-          return const TextStyle(
-            color: Colors.grey,
-            decoration: TextDecoration.lineThrough,
-          );
-        }
-        return const TextStyle();
-      },
-    ),
-  'text/heading': builtInPluginStyle
-    ..update(
-      'textStyle',
-      (_) => (EditorState editorState, Node node) {
-        final headingToFontSize = {
-          'h1': 32.0,
-          'h2': 28.0,
-          'h3': 24.0,
-          'h4': 18.0,
-          'h5': 18.0,
-          'h6': 18.0,
-        };
-        final fontSize = headingToFontSize[node.attributes.heading] ?? 18.0;
-        return TextStyle(fontSize: fontSize, fontWeight: FontWeight.bold);
-      },
-    ),
-  'text/bulleted-list': builtInPluginStyle,
-  'text/number-list': builtInPluginStyle
-    ..update(
-      'iconPadding',
-      (_) => (EditorState editorState, Node node) {
-        return const EdgeInsets.only(left: 5.0, right: 5.0);
-      },
-    ),
-  'text/quote': builtInPluginStyle,
-  'image': builtInPluginStyle,
-};
-
-class BuiltInTextStyle {
-  const BuiltInTextStyle({
-    required this.defaultTextStyle,
-    required this.defaultPlaceholderTextStyle,
+    required this.selectionMenuBackgroundColor,
+    required this.selectionMenuItemTextColor,
+    required this.selectionMenuItemIconColor,
+    required this.selectionMenuItemSelectedTextColor,
+    required this.selectionMenuItemSelectedIconColor,
+    required this.selectionMenuItemSelectedColor,
+    required this.textPadding,
+    required this.textStyle,
+    required this.placeholderTextStyle,
     required this.bold,
     required this.bold,
     required this.italic,
     required this.italic,
     required this.underline,
     required this.underline,
     required this.strikethrough,
     required this.strikethrough,
     required this.href,
     required this.href,
     required this.code,
     required this.code,
-    this.highlightColorHex = '0x6000BCF0',
-    this.lineHeight = 1.5,
+    required this.highlightColorHex,
+    required this.lineHeight,
   });
   });
 
 
-  final TextStyle defaultTextStyle;
-  final TextStyle defaultPlaceholderTextStyle;
-  final TextStyle bold;
-  final TextStyle italic;
-  final TextStyle underline;
-  final TextStyle strikethrough;
-  final TextStyle href;
-  final TextStyle code;
-  final String highlightColorHex;
-  final double lineHeight;
-
-  BuiltInTextStyle.builtIn()
-      : defaultTextStyle = const TextStyle(fontSize: 16.0, color: Colors.black),
-        defaultPlaceholderTextStyle =
-            const TextStyle(fontSize: 16.0, color: Colors.grey),
-        bold = const TextStyle(fontWeight: FontWeight.bold),
-        italic = const TextStyle(fontStyle: FontStyle.italic),
-        underline = const TextStyle(decoration: TextDecoration.underline),
-        strikethrough = const TextStyle(decoration: TextDecoration.lineThrough),
-        href = const TextStyle(
-          color: Colors.blue,
-          decoration: TextDecoration.underline,
-        ),
-        code = const TextStyle(
-          fontFamily: 'monospace',
-          color: Color(0xFF00BCF0),
-          backgroundColor: Color(0xFFE0F8FF),
-        ),
-        highlightColorHex = '0x6000BCF0',
-        lineHeight = 1.5;
-
-  BuiltInTextStyle.builtInDarkMode()
-      : defaultTextStyle = const TextStyle(fontSize: 16.0, color: Colors.white),
-        defaultPlaceholderTextStyle = TextStyle(
-          fontSize: 16.0,
-          color: Colors.white.withOpacity(0.3),
-        ),
-        bold = const TextStyle(fontWeight: FontWeight.bold),
-        italic = const TextStyle(fontStyle: FontStyle.italic),
-        underline = const TextStyle(decoration: TextDecoration.underline),
-        strikethrough = const TextStyle(decoration: TextDecoration.lineThrough),
-        href = const TextStyle(
-          color: Colors.blue,
-          decoration: TextDecoration.underline,
-        ),
-        code = const TextStyle(
-          fontFamily: 'monospace',
-          color: Color(0xFF00BCF0),
-          backgroundColor: Color(0xFFE0F8FF),
-        ),
-        highlightColorHex = '0x6000BCF0',
-        lineHeight = 1.5;
-
-  BuiltInTextStyle copyWith({
-    TextStyle? defaultTextStyle,
-    TextStyle? defaultPlaceholderTextStyle,
+  @override
+  EditorStyle copyWith({
+    EdgeInsets? padding,
+    Color? cursorColor,
+    Color? selectionColor,
+    Color? selectionMenuBackgroundColor,
+    Color? selectionMenuItemTextColor,
+    Color? selectionMenuItemIconColor,
+    Color? selectionMenuItemSelectedTextColor,
+    Color? selectionMenuItemSelectedIconColor,
+    Color? selectionMenuItemSelectedColor,
+    TextStyle? textStyle,
+    TextStyle? placeholderTextStyle,
     TextStyle? bold,
     TextStyle? bold,
     TextStyle? italic,
     TextStyle? italic,
     TextStyle? underline,
     TextStyle? underline,
@@ -206,10 +82,25 @@ class BuiltInTextStyle {
     String? highlightColorHex,
     String? highlightColorHex,
     double? lineHeight,
     double? lineHeight,
   }) {
   }) {
-    return BuiltInTextStyle(
-      defaultTextStyle: defaultTextStyle ?? this.defaultTextStyle,
-      defaultPlaceholderTextStyle:
-          defaultPlaceholderTextStyle ?? this.defaultPlaceholderTextStyle,
+    return EditorStyle(
+      padding: padding ?? this.padding,
+      cursorColor: cursorColor ?? this.cursorColor,
+      selectionColor: selectionColor ?? this.selectionColor,
+      selectionMenuBackgroundColor:
+          selectionMenuBackgroundColor ?? this.selectionMenuBackgroundColor,
+      selectionMenuItemTextColor:
+          selectionMenuItemTextColor ?? this.selectionMenuItemTextColor,
+      selectionMenuItemIconColor:
+          selectionMenuItemIconColor ?? this.selectionMenuItemIconColor,
+      selectionMenuItemSelectedTextColor: selectionMenuItemSelectedTextColor ??
+          this.selectionMenuItemSelectedTextColor,
+      selectionMenuItemSelectedIconColor: selectionMenuItemSelectedIconColor ??
+          this.selectionMenuItemSelectedIconColor,
+      selectionMenuItemSelectedColor:
+          selectionMenuItemSelectedColor ?? this.selectionMenuItemSelectedColor,
+      textPadding: textPadding ?? textPadding,
+      textStyle: textStyle ?? this.textStyle,
+      placeholderTextStyle: placeholderTextStyle ?? this.placeholderTextStyle,
       bold: bold ?? this.bold,
       bold: bold ?? this.bold,
       italic: italic ?? this.italic,
       italic: italic ?? this.italic,
       underline: underline ?? this.underline,
       underline: underline ?? this.underline,
@@ -222,33 +113,87 @@ class BuiltInTextStyle {
   }
   }
 
 
   @override
   @override
-  bool operator ==(Object other) {
-    if (identical(this, other)) return true;
-
-    return other is BuiltInTextStyle &&
-        other.defaultTextStyle == defaultTextStyle &&
-        other.defaultPlaceholderTextStyle == defaultPlaceholderTextStyle &&
-        other.bold == bold &&
-        other.italic == italic &&
-        other.underline == underline &&
-        other.strikethrough == strikethrough &&
-        other.href == href &&
-        other.code == code &&
-        other.highlightColorHex == highlightColorHex &&
-        other.lineHeight == lineHeight;
+  ThemeExtension<EditorStyle> lerp(
+      ThemeExtension<EditorStyle>? other, double t) {
+    if (other == null || other is! EditorStyle) {
+      return this;
+    }
+    return EditorStyle(
+      padding: EdgeInsets.lerp(padding, other.padding, t),
+      cursorColor: Color.lerp(cursorColor, other.cursorColor, t),
+      textPadding: EdgeInsets.lerp(textPadding, other.textPadding, t),
+      selectionColor: Color.lerp(selectionColor, other.selectionColor, t),
+      selectionMenuBackgroundColor: Color.lerp(
+          selectionMenuBackgroundColor, other.selectionMenuBackgroundColor, t),
+      selectionMenuItemTextColor: Color.lerp(
+          selectionMenuItemTextColor, other.selectionMenuItemTextColor, t),
+      selectionMenuItemIconColor: Color.lerp(
+          selectionMenuItemIconColor, other.selectionMenuItemIconColor, t),
+      selectionMenuItemSelectedTextColor: Color.lerp(
+          selectionMenuItemSelectedTextColor,
+          other.selectionMenuItemSelectedTextColor,
+          t),
+      selectionMenuItemSelectedIconColor: Color.lerp(
+          selectionMenuItemSelectedIconColor,
+          other.selectionMenuItemSelectedIconColor,
+          t),
+      selectionMenuItemSelectedColor: Color.lerp(selectionMenuItemSelectedColor,
+          other.selectionMenuItemSelectedColor, t),
+      textStyle: TextStyle.lerp(textStyle, other.textStyle, t),
+      placeholderTextStyle:
+          TextStyle.lerp(placeholderTextStyle, other.placeholderTextStyle, t),
+      bold: TextStyle.lerp(bold, other.bold, t),
+      italic: TextStyle.lerp(italic, other.italic, t),
+      underline: TextStyle.lerp(underline, other.underline, t),
+      strikethrough: TextStyle.lerp(strikethrough, other.strikethrough, t),
+      href: TextStyle.lerp(href, other.href, t),
+      code: TextStyle.lerp(code, other.code, t),
+      highlightColorHex: highlightColorHex,
+      lineHeight: lineHeight,
+    );
   }
   }
 
 
-  @override
-  int get hashCode {
-    return defaultTextStyle.hashCode ^
-        defaultPlaceholderTextStyle.hashCode ^
-        bold.hashCode ^
-        italic.hashCode ^
-        underline.hashCode ^
-        strikethrough.hashCode ^
-        href.hashCode ^
-        code.hashCode ^
-        highlightColorHex.hashCode ^
-        lineHeight.hashCode;
-  }
+  static final light = EditorStyle(
+    padding: const EdgeInsets.fromLTRB(200.0, 0.0, 200.0, 0.0),
+    cursorColor: const Color(0xFF00BCF0),
+    selectionColor: const Color.fromARGB(53, 111, 201, 231),
+    selectionMenuBackgroundColor: const Color(0xFFFFFFFF),
+    selectionMenuItemTextColor: const Color(0xFF333333),
+    selectionMenuItemIconColor: const Color(0xFF333333),
+    selectionMenuItemSelectedTextColor: const Color(0xFF333333),
+    selectionMenuItemSelectedIconColor: const Color(0xFF333333),
+    selectionMenuItemSelectedColor: const Color(0xFFE0F8FF),
+    textPadding: const EdgeInsets.symmetric(vertical: 8.0),
+    textStyle: const TextStyle(fontSize: 16.0, color: Colors.black),
+    placeholderTextStyle: const TextStyle(fontSize: 16.0, color: Colors.grey),
+    bold: const TextStyle(fontWeight: FontWeight.bold),
+    italic: const TextStyle(fontStyle: FontStyle.italic),
+    underline: const TextStyle(decoration: TextDecoration.underline),
+    strikethrough: const TextStyle(decoration: TextDecoration.lineThrough),
+    href: const TextStyle(
+      color: Colors.blue,
+      decoration: TextDecoration.underline,
+    ),
+    code: const TextStyle(
+      fontFamily: 'monospace',
+      color: Color(0xFF00BCF0),
+      backgroundColor: Color(0xFFE0F8FF),
+    ),
+    highlightColorHex: '0x6000BCF0',
+    lineHeight: 1.5,
+  );
+
+  static final dark = light.copyWith(
+    textStyle: const TextStyle(fontSize: 16.0, color: Colors.white),
+    placeholderTextStyle: TextStyle(
+      fontSize: 16.0,
+      color: Colors.white.withOpacity(0.3),
+    ),
+    selectionMenuBackgroundColor: const Color(0xFF282E3A),
+    selectionMenuItemTextColor: const Color(0xFFBBC3CD),
+    selectionMenuItemIconColor: const Color(0xFFBBC3CD),
+    selectionMenuItemSelectedTextColor: const Color(0xFF131720),
+    selectionMenuItemSelectedIconColor: const Color(0xFF131720),
+    selectionMenuItemSelectedColor: const Color(0xFF00BCF0),
+  );
 }
 }

+ 327 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/plugin_styles.dart

@@ -0,0 +1,327 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_editor/src/infra/flowy_svg.dart';
+import 'package:flutter/material.dart';
+
+Iterable<ThemeExtension<dynamic>> get lightPlguinStyleExtension => [
+      HeadingPluginStyle.light,
+      CheckboxPluginStyle.light,
+      NumberListPluginStyle.light,
+      QuotedTextPluginStyle.light,
+    ];
+
+Iterable<ThemeExtension<dynamic>> get darkPlguinStyleExtension => [
+      HeadingPluginStyle.dark,
+      CheckboxPluginStyle.dark,
+      NumberListPluginStyle.dark,
+      QuotedTextPluginStyle.dark,
+      BulletedListPluginStyle.dark,
+    ];
+
+typedef TextStyleCustomizer = TextStyle Function(
+    EditorState editorState, TextNode textNode);
+typedef PaddingCustomizer = EdgeInsets Function(
+    EditorState editorState, TextNode textNode);
+typedef IconCustomizer = Widget Function(
+    EditorState editorState, TextNode textNode);
+
+class HeadingPluginStyle extends ThemeExtension<HeadingPluginStyle> {
+  const HeadingPluginStyle({
+    required this.textStyle,
+    required this.padding,
+  });
+
+  final TextStyleCustomizer textStyle;
+  final PaddingCustomizer padding;
+
+  @override
+  HeadingPluginStyle copyWith({
+    TextStyleCustomizer? textStyle,
+    PaddingCustomizer? padding,
+  }) {
+    return HeadingPluginStyle(
+      textStyle: textStyle ?? this.textStyle,
+      padding: padding ?? this.padding,
+    );
+  }
+
+  @override
+  ThemeExtension<HeadingPluginStyle> lerp(
+      ThemeExtension<HeadingPluginStyle>? other, double t) {
+    if (other is! HeadingPluginStyle) {
+      return this;
+    }
+    return HeadingPluginStyle(
+      textStyle: other.textStyle,
+      padding: other.padding,
+    );
+  }
+
+  static final light = HeadingPluginStyle(
+    padding: (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
+    textStyle: (editorState, textNode) {
+      final headingToFontSize = {
+        'h1': 32.0,
+        'h2': 28.0,
+        'h3': 24.0,
+        'h4': 18.0,
+        'h5': 18.0,
+        'h6': 18.0,
+      };
+      final fontSize = headingToFontSize[textNode.attributes.heading] ?? 18.0;
+      return TextStyle(
+        fontSize: fontSize,
+        fontWeight: FontWeight.bold,
+      );
+    },
+  );
+
+  static final dark = light;
+}
+
+class CheckboxPluginStyle extends ThemeExtension<CheckboxPluginStyle> {
+  const CheckboxPluginStyle({
+    required this.textStyle,
+    required this.padding,
+    required this.icon,
+  });
+
+  final TextStyleCustomizer textStyle;
+  final PaddingCustomizer padding;
+  final IconCustomizer icon;
+
+  @override
+  CheckboxPluginStyle copyWith({
+    TextStyleCustomizer? textStyle,
+    PaddingCustomizer? padding,
+    IconCustomizer? icon,
+  }) {
+    return CheckboxPluginStyle(
+      textStyle: textStyle ?? this.textStyle,
+      padding: padding ?? this.padding,
+      icon: icon ?? this.icon,
+    );
+  }
+
+  @override
+  ThemeExtension<CheckboxPluginStyle> lerp(
+      ThemeExtension<CheckboxPluginStyle>? other, double t) {
+    if (other is! CheckboxPluginStyle) {
+      return this;
+    }
+    return CheckboxPluginStyle(
+      textStyle: other.textStyle,
+      padding: other.padding,
+      icon: other.icon,
+    );
+  }
+
+  static final light = CheckboxPluginStyle(
+    padding: (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
+    textStyle: (editorState, textNode) => const TextStyle(),
+    icon: (editorState, textNode) {
+      final isCheck = textNode.attributes.check;
+      const iconSize = Size.square(20.0);
+      const iconPadding = EdgeInsets.only(right: 5.0);
+      return FlowySvg(
+        width: iconSize.width,
+        height: iconSize.height,
+        padding: iconPadding,
+        name: isCheck ? 'check' : 'uncheck',
+      );
+    },
+  );
+
+  static final dark = light;
+}
+
+class BulletedListPluginStyle extends ThemeExtension<BulletedListPluginStyle> {
+  const BulletedListPluginStyle({
+    required this.textStyle,
+    required this.padding,
+    required this.icon,
+  });
+
+  final TextStyleCustomizer textStyle;
+  final PaddingCustomizer padding;
+  final IconCustomizer icon;
+
+  @override
+  BulletedListPluginStyle copyWith({
+    TextStyleCustomizer? textStyle,
+    PaddingCustomizer? padding,
+    IconCustomizer? icon,
+  }) {
+    return BulletedListPluginStyle(
+      textStyle: textStyle ?? this.textStyle,
+      padding: padding ?? this.padding,
+      icon: icon ?? this.icon,
+    );
+  }
+
+  @override
+  ThemeExtension<BulletedListPluginStyle> lerp(
+      ThemeExtension<BulletedListPluginStyle>? other, double t) {
+    if (other is! BulletedListPluginStyle) {
+      return this;
+    }
+    return BulletedListPluginStyle(
+      textStyle: other.textStyle,
+      padding: other.padding,
+      icon: other.icon,
+    );
+  }
+
+  static final light = BulletedListPluginStyle(
+    padding: (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
+    textStyle: (_, __) => const TextStyle(),
+    icon: (_, __) {
+      const iconSize = Size.square(20.0);
+      const iconPadding = EdgeInsets.only(right: 5.0);
+      return FlowySvg(
+        width: iconSize.width,
+        height: iconSize.height,
+        padding: iconPadding,
+        color: Colors.black,
+        name: 'point',
+      );
+    },
+  );
+
+  static final dark = light.copyWith(icon: (_, __) {
+    const iconSize = Size.square(20.0);
+    const iconPadding = EdgeInsets.only(right: 5.0);
+    return FlowySvg(
+      width: iconSize.width,
+      height: iconSize.height,
+      padding: iconPadding,
+      color: Colors.white,
+      name: 'point',
+    );
+  });
+}
+
+class NumberListPluginStyle extends ThemeExtension<NumberListPluginStyle> {
+  const NumberListPluginStyle({
+    required this.textStyle,
+    required this.padding,
+    required this.icon,
+  });
+
+  final TextStyleCustomizer textStyle;
+  final PaddingCustomizer padding;
+  final IconCustomizer icon;
+
+  @override
+  NumberListPluginStyle copyWith({
+    TextStyleCustomizer? textStyle,
+    PaddingCustomizer? padding,
+    IconCustomizer? icon,
+  }) {
+    return NumberListPluginStyle(
+      textStyle: textStyle ?? this.textStyle,
+      padding: padding ?? this.padding,
+      icon: icon ?? this.icon,
+    );
+  }
+
+  @override
+  ThemeExtension<NumberListPluginStyle> lerp(
+    ThemeExtension<NumberListPluginStyle>? other,
+    double t,
+  ) {
+    if (other is! NumberListPluginStyle) {
+      return this;
+    }
+    return NumberListPluginStyle(
+      textStyle: other.textStyle,
+      padding: other.padding,
+      icon: other.icon,
+    );
+  }
+
+  static final light = NumberListPluginStyle(
+    padding: (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
+    textStyle: (_, __) => const TextStyle(),
+    icon: (_, textNode) {
+      const iconPadding = EdgeInsets.only(left: 5.0, right: 5.0);
+      return Container(
+        padding: iconPadding,
+        child: Text(
+          '${textNode.attributes.number.toString()}.',
+          style: const TextStyle(
+            fontSize: 16,
+            color: Colors.black,
+          ),
+        ),
+      );
+    },
+  );
+
+  static final dark = light.copyWith(icon: (editorState, textNode) {
+    const iconPadding = EdgeInsets.only(left: 5.0, right: 5.0);
+    return Container(
+      padding: iconPadding,
+      child: Text(
+        '${textNode.attributes.number.toString()}.',
+        style: const TextStyle(
+          fontSize: 16,
+          color: Colors.white,
+        ),
+      ),
+    );
+  });
+}
+
+class QuotedTextPluginStyle extends ThemeExtension<QuotedTextPluginStyle> {
+  const QuotedTextPluginStyle({
+    required this.textStyle,
+    required this.padding,
+    required this.icon,
+  });
+
+  final TextStyleCustomizer textStyle;
+  final PaddingCustomizer padding;
+  final IconCustomizer icon;
+
+  @override
+  QuotedTextPluginStyle copyWith({
+    TextStyleCustomizer? textStyle,
+    PaddingCustomizer? padding,
+    IconCustomizer? icon,
+  }) {
+    return QuotedTextPluginStyle(
+      textStyle: textStyle ?? this.textStyle,
+      padding: padding ?? this.padding,
+      icon: icon ?? this.icon,
+    );
+  }
+
+  @override
+  ThemeExtension<QuotedTextPluginStyle> lerp(
+      ThemeExtension<QuotedTextPluginStyle>? other, double t) {
+    if (other is! QuotedTextPluginStyle) {
+      return this;
+    }
+    return QuotedTextPluginStyle(
+      textStyle: other.textStyle,
+      padding: other.padding,
+      icon: other.icon,
+    );
+  }
+
+  static final light = QuotedTextPluginStyle(
+    padding: (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
+    textStyle: (_, __) => const TextStyle(),
+    icon: (_, __) {
+      const iconSize = Size.square(20.0);
+      const iconPadding = EdgeInsets.only(right: 5.0);
+      return FlowySvg(
+        width: iconSize.width,
+        padding: iconPadding,
+        name: 'quote',
+      );
+    },
+  );
+
+  static final dark = light;
+}

+ 2 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart

@@ -259,7 +259,7 @@ List<ToolbarItem> defaultToolbarItems = [
     ),
     ),
     handler: (editorState, context) => formatHighlight(
     handler: (editorState, context) => formatHighlight(
       editorState,
       editorState,
-      editorState.editorStyle.textStyle.highlightColorHex,
+      editorState.editorStyle.highlightColorHex!,
     ),
     ),
   ),
   ),
 ];
 ];
@@ -348,6 +348,7 @@ void showLinkMenu(
       child: Material(
       child: Material(
         child: LinkMenu(
         child: LinkMenu(
           linkText: linkText,
           linkText: linkText,
+          editorState: editorState,
           onOpenLink: () async {
           onOpenLink: () async {
             await safeLaunchUrl(linkText);
             await safeLaunchUrl(linkText);
           },
           },

+ 1 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart

@@ -25,6 +25,7 @@ class ToolbarItemWidget extends StatelessWidget {
         child: MouseRegion(
         child: MouseRegion(
           cursor: SystemMouseCursors.click,
           cursor: SystemMouseCursors.click,
           child: IconButton(
           child: IconButton(
+            hoverColor: Colors.transparent,
             highlightColor: Colors.transparent,
             highlightColor: Colors.transparent,
             padding: EdgeInsets.zero,
             padding: EdgeInsets.zero,
             icon: item.iconBuilder(isHighlight),
             icon: item.iconBuilder(isHighlight),

+ 36 - 19
frontend/app_flowy/packages/appflowy_editor/lib/src/service/context_menu/context_menu.dart

@@ -30,26 +30,43 @@ class ContextMenu extends StatelessWidget {
     final children = <Widget>[];
     final children = <Widget>[];
     for (var i = 0; i < items.length; i++) {
     for (var i = 0; i < items.length; i++) {
       for (var j = 0; j < items[i].length; j++) {
       for (var j = 0; j < items[i].length; j++) {
+        var onHover = false;
         children.add(
         children.add(
-          Material(
-            child: InkWell(
-              hoverColor: const Color(0xFFE0F8FF),
-              customBorder: RoundedRectangleBorder(
-                borderRadius: BorderRadius.circular(6),
-              ),
-              onTap: () {
-                items[i][j].onPressed(editorState);
-                onPressed();
-              },
-              child: Padding(
-                padding: const EdgeInsets.all(8.0),
-                child: Text(
-                  items[i][j].name,
-                  textAlign: TextAlign.start,
-                  style: const TextStyle(fontSize: 14),
+          StatefulBuilder(
+            builder: (BuildContext context, setState) {
+              return Material(
+                color: editorState.editorStyle.selectionMenuBackgroundColor,
+                child: InkWell(
+                  hoverColor:
+                      editorState.editorStyle.selectionMenuItemSelectedColor,
+                  customBorder: RoundedRectangleBorder(
+                    borderRadius: BorderRadius.circular(6),
+                  ),
+                  onTap: () {
+                    items[i][j].onPressed(editorState);
+                    onPressed();
+                  },
+                  onHover: (value) => setState(() {
+                    onHover = value;
+                  }),
+                  child: Padding(
+                    padding: const EdgeInsets.all(8.0),
+                    child: Text(
+                      items[i][j].name,
+                      textAlign: TextAlign.start,
+                      style: TextStyle(
+                        fontSize: 14,
+                        color: onHover
+                            ? editorState
+                                .editorStyle.selectionMenuItemSelectedTextColor
+                            : editorState
+                                .editorStyle.selectionMenuItemTextColor,
+                      ),
+                    ),
+                  ),
                 ),
                 ),
-              ),
-            ),
+              );
+            },
           ),
           ),
         );
         );
       }
       }
@@ -67,7 +84,7 @@ class ContextMenu extends StatelessWidget {
           minWidth: 140,
           minWidth: 140,
         ),
         ),
         decoration: BoxDecoration(
         decoration: BoxDecoration(
-          color: Colors.white,
+          color: editorState.editorStyle.selectionMenuBackgroundColor,
           boxShadow: [
           boxShadow: [
             BoxShadow(
             BoxShadow(
               blurRadius: 5,
               blurRadius: 5,

+ 46 - 44
frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart

@@ -1,12 +1,9 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/src/flutter/overlay.dart';
 import 'package:appflowy_editor/src/flutter/overlay.dart';
 import 'package:appflowy_editor/src/render/image/image_node_builder.dart';
 import 'package:appflowy_editor/src/render/image/image_node_builder.dart';
-import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
-import 'package:appflowy_editor/src/render/style/editor_style.dart';
 import 'package:appflowy_editor/src/service/shortcut_event/built_in_shortcut_events.dart';
 import 'package:appflowy_editor/src/service/shortcut_event/built_in_shortcut_events.dart';
-import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart';
 import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
 import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
 
 
-import 'package:appflowy_editor/src/editor_state.dart';
 import 'package:appflowy_editor/src/render/editor/editor_entry.dart';
 import 'package:appflowy_editor/src/render/editor/editor_entry.dart';
 import 'package:appflowy_editor/src/render/rich_text/bulleted_list_text.dart';
 import 'package:appflowy_editor/src/render/rich_text/bulleted_list_text.dart';
 import 'package:appflowy_editor/src/render/rich_text/checkbox_text.dart';
 import 'package:appflowy_editor/src/render/rich_text/checkbox_text.dart';
@@ -14,12 +11,6 @@ import 'package:appflowy_editor/src/render/rich_text/heading_text.dart';
 import 'package:appflowy_editor/src/render/rich_text/number_list_text.dart';
 import 'package:appflowy_editor/src/render/rich_text/number_list_text.dart';
 import 'package:appflowy_editor/src/render/rich_text/quoted_text.dart';
 import 'package:appflowy_editor/src/render/rich_text/quoted_text.dart';
 import 'package:appflowy_editor/src/render/rich_text/rich_text.dart';
 import 'package:appflowy_editor/src/render/rich_text/rich_text.dart';
-import 'package:appflowy_editor/src/service/input_service.dart';
-import 'package:appflowy_editor/src/service/keyboard_service.dart';
-import 'package:appflowy_editor/src/service/render_plugin_service.dart';
-import 'package:appflowy_editor/src/service/scroll_service.dart';
-import 'package:appflowy_editor/src/service/selection_service.dart';
-import 'package:appflowy_editor/src/service/toolbar_service.dart';
 
 
 NodeWidgetBuilders defaultBuilders = {
 NodeWidgetBuilders defaultBuilders = {
   'editor': EditorEntryWidgetBuilder(),
   'editor': EditorEntryWidgetBuilder(),
@@ -33,15 +24,21 @@ NodeWidgetBuilders defaultBuilders = {
 };
 };
 
 
 class AppFlowyEditor extends StatefulWidget {
 class AppFlowyEditor extends StatefulWidget {
-  const AppFlowyEditor({
+  AppFlowyEditor({
     Key? key,
     Key? key,
     required this.editorState,
     required this.editorState,
     this.customBuilders = const {},
     this.customBuilders = const {},
     this.shortcutEvents = const [],
     this.shortcutEvents = const [],
     this.selectionMenuItems = const [],
     this.selectionMenuItems = const [],
     this.editable = true,
     this.editable = true,
-    required this.editorStyle,
-  }) : super(key: key);
+    ThemeData? themeData,
+  }) : super(key: key) {
+    this.themeData = themeData ??
+        ThemeData.light().copyWith(extensions: [
+          ...lightEditorStyleExtension,
+          ...lightPlguinStyleExtension,
+        ]);
+  }
 
 
   final EditorState editorState;
   final EditorState editorState;
 
 
@@ -53,7 +50,7 @@ class AppFlowyEditor extends StatefulWidget {
 
 
   final List<SelectionMenuItem> selectionMenuItems;
   final List<SelectionMenuItem> selectionMenuItems;
 
 
-  final EditorStyle editorStyle;
+  late final ThemeData themeData;
 
 
   final bool editable;
   final bool editable;
 
 
@@ -65,13 +62,15 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
   Widget? services;
   Widget? services;
 
 
   EditorState get editorState => widget.editorState;
   EditorState get editorState => widget.editorState;
+  EditorStyle get editorStyle =>
+      editorState.themeData.extension<EditorStyle>() ?? EditorStyle.light;
 
 
   @override
   @override
   void initState() {
   void initState() {
     super.initState();
     super.initState();
 
 
     editorState.selectionMenuItems = widget.selectionMenuItems;
     editorState.selectionMenuItems = widget.selectionMenuItems;
-    editorState.editorStyle = widget.editorStyle;
+    editorState.themeData = widget.themeData;
     editorState.service.renderPluginService = _createRenderPlugin();
     editorState.service.renderPluginService = _createRenderPlugin();
     editorState.editable = widget.editable;
     editorState.editable = widget.editable;
   }
   }
@@ -85,7 +84,7 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
       editorState.service.renderPluginService = _createRenderPlugin();
       editorState.service.renderPluginService = _createRenderPlugin();
     }
     }
 
 
-    editorState.editorStyle = widget.editorStyle;
+    editorState.themeData = widget.themeData;
     editorState.editable = widget.editable;
     editorState.editable = widget.editable;
     services = null;
     services = null;
   }
   }
@@ -102,38 +101,41 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
     );
     );
   }
   }
 
 
-  AppFlowyScroll _buildServices(BuildContext context) {
-    return AppFlowyScroll(
-      key: editorState.service.scrollServiceKey,
-      child: Padding(
-        padding: widget.editorStyle.padding,
-        child: AppFlowySelection(
-          key: editorState.service.selectionServiceKey,
-          cursorColor: widget.editorStyle.cursorColor,
-          selectionColor: widget.editorStyle.selectionColor,
-          editorState: editorState,
-          editable: widget.editable,
-          child: AppFlowyInput(
-            key: editorState.service.inputServiceKey,
+  Widget _buildServices(BuildContext context) {
+    return Theme(
+      data: widget.themeData,
+      child: AppFlowyScroll(
+        key: editorState.service.scrollServiceKey,
+        child: Padding(
+          padding: editorStyle.padding!,
+          child: AppFlowySelection(
+            key: editorState.service.selectionServiceKey,
+            cursorColor: editorStyle.cursorColor!,
+            selectionColor: editorStyle.selectionColor!,
             editorState: editorState,
             editorState: editorState,
             editable: widget.editable,
             editable: widget.editable,
-            child: AppFlowyKeyboard(
-              key: editorState.service.keyboardServiceKey,
-              editable: widget.editable,
-              shortcutEvents: [
-                ...widget.shortcutEvents,
-                ...builtInShortcutEvents,
-              ],
+            child: AppFlowyInput(
+              key: editorState.service.inputServiceKey,
               editorState: editorState,
               editorState: editorState,
-              child: FlowyToolbar(
-                key: editorState.service.toolbarServiceKey,
+              editable: widget.editable,
+              child: AppFlowyKeyboard(
+                key: editorState.service.keyboardServiceKey,
+                editable: widget.editable,
+                shortcutEvents: [
+                  ...widget.shortcutEvents,
+                  ...builtInShortcutEvents,
+                ],
                 editorState: editorState,
                 editorState: editorState,
-                child:
-                    editorState.service.renderPluginService.buildPluginWidget(
-                  NodeWidgetContext(
-                    context: context,
-                    node: editorState.document.root,
-                    editorState: editorState,
+                child: FlowyToolbar(
+                  key: editorState.service.toolbarServiceKey,
+                  editorState: editorState,
+                  child:
+                      editorState.service.renderPluginService.buildPluginWidget(
+                    NodeWidgetContext(
+                      context: context,
+                      node: editorState.document.root,
+                      editorState: editorState,
+                    ),
                   ),
                   ),
                 ),
                 ),
               ),
               ),

+ 17 - 4
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart

@@ -41,7 +41,10 @@ void _handleCopy(EditorState editorState) async {
       Log.keyboard.debug('copy html: $htmlString');
       Log.keyboard.debug('copy html: $htmlString');
       RichClipboard.setData(RichClipboardData(
       RichClipboard.setData(RichClipboardData(
         html: htmlString,
         html: htmlString,
-        text: textNode.toPlainText(),
+        text: textNode.toPlainText().substring(
+              selection.startIndex,
+              selection.endIndex,
+            ),
       ));
       ));
     } else {
     } else {
       Log.keyboard.debug('unimplemented: copy non-text');
       Log.keyboard.debug('unimplemented: copy non-text');
@@ -63,9 +66,19 @@ void _handleCopy(EditorState editorState) async {
     startOffset: selection.start.offset,
     startOffset: selection.start.offset,
     endOffset: selection.end.offset,
     endOffset: selection.end.offset,
   ).toHTMLString();
   ).toHTMLString();
-  final text = nodes
-      .map((node) => node is TextNode ? node.toPlainText() : '\n')
-      .join('\n');
+  var text = '';
+  for (final node in nodes) {
+    if (node is TextNode) {
+      if (node.path == selection.start.path) {
+        text += node.toPlainText().substring(selection.start.offset);
+      } else if (node.path == selection.end.path) {
+        text += node.toPlainText().substring(0, selection.end.offset);
+      } else {
+        text += node.toPlainText();
+      }
+    }
+    text += '\n';
+  }
   RichClipboard.setData(RichClipboardData(html: html, text: text));
   RichClipboard.setData(RichClipboardData(html: html, text: text));
 }
 }
 
 

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/format_style_handler.dart

@@ -57,7 +57,7 @@ ShortcutEventHandler formatHighlightEventHandler = (editorState, event) {
   }
   }
   formatHighlight(
   formatHighlight(
     editorState,
     editorState,
-    editorState.editorStyle.textStyle.highlightColorHex,
+    editorState.editorStyle.highlightColorHex!,
   );
   );
   return KeyEventResult.handled;
   return KeyEventResult.handled;
 };
 };

+ 0 - 1
frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart

@@ -39,7 +39,6 @@ class EditorWidgetTester {
       home: Scaffold(
       home: Scaffold(
         body: AppFlowyEditor(
         body: AppFlowyEditor(
           editorState: _editorState,
           editorState: _editorState,
-          editorStyle: EditorStyle.defaultStyle(),
         ),
         ),
       ),
       ),
     );
     );

+ 3 - 2
frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart

@@ -49,10 +49,11 @@ void main() async {
         final editorRect = tester.getRect(editorFinder);
         final editorRect = tester.getRect(editorFinder);
 
 
         final leftImageRect = tester.getRect(imageFinder.at(0));
         final leftImageRect = tester.getRect(imageFinder.at(0));
-        expect(leftImageRect.left, editor.editorState.editorStyle.padding.left);
+        expect(
+            leftImageRect.left, editor.editorState.editorStyle.padding!.left);
         final rightImageRect = tester.getRect(imageFinder.at(2));
         final rightImageRect = tester.getRect(imageFinder.at(2));
         expect(rightImageRect.right,
         expect(rightImageRect.right,
-            editorRect.right - editor.editorState.editorStyle.padding.right);
+            editorRect.right - editor.editorState.editorStyle.padding!.right);
         final centerImageRect = tester.getRect(imageFinder.at(1));
         final centerImageRect = tester.getRect(imageFinder.at(1));
         expect(centerImageRect.left,
         expect(centerImageRect.left,
             (leftImageRect.left + rightImageRect.left) / 2.0);
             (leftImageRect.left + rightImageRect.left) / 2.0);

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart

@@ -137,7 +137,7 @@ Future<EditorWidgetTester> _prepare(WidgetTester tester) async {
   );
   );
 
 
   for (final item in defaultSelectionMenuItems) {
   for (final item in defaultSelectionMenuItems) {
-    expect(find.byWidget(item.icon), findsOneWidget);
+    expect(find.text(item.name()), findsOneWidget);
   }
   }
 
 
   return Future.value(editor);
   return Future.value(editor);

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart

@@ -30,7 +30,7 @@ void main() async {
       );
       );
 
 
       for (final item in defaultSelectionMenuItems) {
       for (final item in defaultSelectionMenuItems) {
-        expect(find.byWidget(item.icon), findsOneWidget);
+        expect(find.text(item.name()), findsOneWidget);
       }
       }
 
 
       await editor.updateSelection(Selection.single(path: [1], startOffset: 0));
       await editor.updateSelection(Selection.single(path: [1], startOffset: 0));