diff --git a/frontend/app_flowy/lib/plugins/doc/document_page.dart b/frontend/app_flowy/lib/plugins/doc/document_page.dart index fca1fa219b..9d8df7d98a 100644 --- a/frontend/app_flowy/lib/plugins/doc/document_page.dart +++ b/frontend/app_flowy/lib/plugins/doc/document_page.dart @@ -93,15 +93,20 @@ class _DocumentPageState extends State { } Widget _renderAppFlowyEditor(EditorState editorState) { + final theme = Theme.of(context); final editor = AppFlowyEditor( editorState: editorState, - editorStyle: customEditorStyle(context), customBuilders: { 'horizontal_rule': HorizontalRuleWidgetBuilder(), }, shortcutEvents: [ insertHorizontalRule, ], + themeData: theme.copyWith(extensions: [ + ...theme.extensions.values, + customEditorTheme(context), + ...customPluginTheme(context), + ]), ); return Expanded( child: SizedBox.expand( diff --git a/frontend/app_flowy/lib/plugins/doc/editor_styles.dart b/frontend/app_flowy/lib/plugins/doc/editor_styles.dart index dc472aeaf1..0c503e19cb 100644 --- a/frontend/app_flowy/lib/plugins/doc/editor_styles.dart +++ b/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:provider/provider.dart'; -EditorStyle customEditorStyle(BuildContext context) { +const _baseFontSize = 14.0; + +EditorStyle customEditorTheme(BuildContext context) { final theme = context.watch(); - 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, + fontSize: _baseFontSize, ), - bold: textStyle.bold.copyWith( + placeholderTextStyle: editorStyle.placeholderTextStyle?.copyWith( + fontFamily: 'poppins', + fontSize: _baseFontSize, + ), + bold: editorStyle.bold?.copyWith( 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> customPluginTheme(BuildContext context) { + final theme = context.watch(); + 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); } diff --git a/frontend/app_flowy/lib/plugins/doc/presentation/plugins/horizontal_rule_node_widget.dart b/frontend/app_flowy/lib/plugins/doc/presentation/plugins/horizontal_rule_node_widget.dart index c38cc0846c..c3d4cbeb35 100644 --- a/frontend/app_flowy/lib/plugins/doc/presentation/plugins/horizontal_rule_node_widget.dart +++ b/frontend/app_flowy/lib/plugins/doc/presentation/plugins/horizontal_rule_node_widget.dart @@ -38,9 +38,11 @@ ShortcutEventHandler _insertHorzaontalRule = (editorState, event) { SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem( name: () => 'Horizontal rule', - icon: const Icon( + icon: (editorState, onSelected) => Icon( Icons.horizontal_rule, - color: Colors.black, + color: onSelected + ? editorState.editorStyle.selectionMenuItemSelectedIconColor + : editorState.editorStyle.selectionMenuItemIconColor, size: 18.0, ), keywords: ['horizontal rule'], diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index 488839a980..128f227676 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/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:flutter_localizations/flutter_localizations.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:path_provider/path_provider.dart'; import 'package:universal_html/html.dart' as html; @@ -38,7 +37,10 @@ class MyApp extends StatelessWidget { debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.blue, + // extensions: [HeadingPluginStyle.light], ), + darkTheme: ThemeData.dark(), + themeMode: ThemeMode.dark, home: const MyHomePage(title: 'AppFlowyEditor Example'), ); } @@ -56,7 +58,6 @@ class _MyHomePageState extends State { int _pageIndex = 0; EditorState? _editorState; bool darkMode = false; - EditorStyle _editorStyle = EditorStyle.defaultStyle(); Future? _jsonString; @override @@ -125,12 +126,31 @@ class _MyHomePageState extends State { _editorState!.transactionStream.listen((event) { 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( color: darkMode ? Colors.black : Colors.white, width: MediaQuery.of(context).size.width, child: AppFlowyEditor( editorState: _editorState!, - editorStyle: _editorStyle, + themeData: themeData, editable: true, customBuilders: { 'text/code_block': CodeBlockNodeWidgetBuilder(), @@ -186,8 +206,6 @@ class _MyHomePageState extends State { icon: const Icon(Icons.color_lens), onPressed: () { setState(() { - _editorStyle = - darkMode ? EditorStyle.defaultStyle() : _customizedStyle(); darkMode = !darkMode; }); }, @@ -256,44 +274,4 @@ class _MyHomePageState extends State { }); } } - - 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, - ); - }; - }, - ), - }, - ); - } } diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart index a82d20157e..5ecf4d4ed8 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart +++ b/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( name: () => 'Code Block', - icon: const Icon( + icon: (_, __) => const Icon( Icons.abc, color: Colors.black, size: 18.0, @@ -167,7 +167,7 @@ class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge> textNode: widget.textNode, editorState: widget.editorState, textSpanDecorator: (textSpan) => TextSpan( - style: widget.editorState.editorStyle.textStyle.defaultTextStyle, + style: widget.editorState.editorStyle.textStyle, children: codeTextSpan, ), ), diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/horizontal_rule_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/horizontal_rule_node_widget.dart index c38cc0846c..0ca302de18 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/horizontal_rule_node_widget.dart +++ b/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( name: () => 'Horizontal rule', - icon: const Icon( + icon: (_, __) => const Icon( Icons.horizontal_rule, color: Colors.black, size: 18.0, diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart index a6b958b0ed..c9b0e8d478 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart +++ b/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( name: () => 'Tex', - icon: const Icon( + icon: (_, __) => const Icon( Icons.text_fields_rounded, color: Colors.black, size: 18.0, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart index 04b2714879..29cb9f87f6 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart +++ b/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/selection_menu/selection_menu_widget.dart'; export 'src/l10n/l10n.dart'; +export 'src/render/style/plugin_styles.dart'; +export 'src/render/style/editor_style.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart index 872dad8e7a..95bab3c231 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart @@ -59,13 +59,14 @@ class EditorState { /// Stores the selection menu items. List selectionMenuItems = []; - /// Stores the editor style. - EditorStyle editorStyle = EditorStyle.defaultStyle(); - /// Operation stream. Stream get transactionStream => _observer.stream; final StreamController _observer = StreamController.broadcast(); + late ThemeData themeData; + EditorStyle get editorStyle => + themeData.extension() ?? EditorStyle.light; + final UndoManager undoManager = UndoManager(); Selection? _cursorSelection; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/theme_extension.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/theme_extension.dart new file mode 100644 index 0000000000..9b8f01aafa --- /dev/null +++ b/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() { + if (extensions.containsKey(T)) { + return extensions[T] as T; + } + return null; + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart index bb02c74600..078609db91 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart +++ b/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/infra/flowy_svg.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'; OverlayEntry? _imageUploadMenu; @@ -20,6 +21,7 @@ void showImageUploadMenu( left: menuService.topLeft.dx, child: Material( child: ImageUploadMenu( + editorState: editorState, onSubmitted: (text) { // _dismissImageUploadMenu(); editorState.insertImageNode(text); @@ -53,10 +55,12 @@ class ImageUploadMenu extends StatefulWidget { Key? key, required this.onSubmitted, required this.onUpload, + this.editorState, }) : super(key: key); final void Function(String text) onSubmitted; final void Function(String text) onUpload; + final EditorState? editorState; @override State createState() => _ImageUploadMenuState(); @@ -66,6 +70,8 @@ class _ImageUploadMenuState extends State { final _textEditingController = TextEditingController(); final _focusNode = FocusNode(); + EditorStyle? get style => widget.editorState?.editorStyle; + @override void initState() { super.initState(); @@ -84,7 +90,7 @@ class _ImageUploadMenuState extends State { width: 300, padding: const EdgeInsets.all(24.0), decoration: BoxDecoration( - color: Colors.white, + color: style?.selectionMenuBackgroundColor ?? Colors.white, boxShadow: [ BoxShadow( blurRadius: 5, @@ -108,12 +114,12 @@ class _ImageUploadMenuState extends State { } Widget _buildHeader(BuildContext context) { - return const Text( + return Text( 'URL Image', textAlign: TextAlign.left, style: TextStyle( fontSize: 14.0, - color: Colors.black, + color: style?.selectionMenuItemTextColor ?? Colors.black, fontWeight: FontWeight.w500, ), ); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart index 3a1785391b..58e8111793 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart +++ b/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/render/style/editor_style.dart'; import 'package:flutter/material.dart'; class LinkMenu extends StatefulWidget { const LinkMenu({ Key? key, this.linkText, + this.editorState, required this.onSubmitted, required this.onOpenLink, required this.onCopyLink, @@ -13,6 +16,7 @@ class LinkMenu extends StatefulWidget { }) : super(key: key); final String? linkText; + final EditorState? editorState; final void Function(String text) onSubmitted; final VoidCallback onOpenLink; final VoidCallback onCopyLink; @@ -27,6 +31,8 @@ class _LinkMenuState extends State { final _textEditingController = TextEditingController(); final _focusNode = FocusNode(); + EditorStyle? get style => widget.editorState?.editorStyle; + @override void initState() { super.initState(); @@ -48,7 +54,7 @@ class _LinkMenuState extends State { width: 350, child: Container( decoration: BoxDecoration( - color: Colors.white, + color: style?.selectionMenuBackgroundColor ?? Colors.white, boxShadow: [ BoxShadow( blurRadius: 5, @@ -71,17 +77,19 @@ class _LinkMenuState extends State { if (widget.linkText != null) ...[ _buildIconButton( iconName: 'link', + color: style?.selectionMenuItemIconColor, text: 'Open link', onPressed: widget.onOpenLink, ), _buildIconButton( iconName: 'copy', - color: Colors.black, + color: style?.selectionMenuItemIconColor, text: 'Copy link', onPressed: widget.onCopyLink, ), _buildIconButton( iconName: 'delete', + color: style?.selectionMenuItemIconColor, text: 'Remove link', onPressed: widget.onRemoveLink, ), @@ -154,8 +162,8 @@ class _LinkMenuState extends State { label: Text( text, textAlign: TextAlign.left, - style: const TextStyle( - color: Colors.black, + style: TextStyle( + color: style?.selectionMenuItemTextColor ?? Colors.black, fontSize: 14.0, ), ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/built_in_text_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/built_in_text_widget.dart index a0a65d9583..7fb2cee2cb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/built_in_text_widget.dart +++ b/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; } -mixin BuiltInStyleMixin on State { - 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 on State implements DefaultSelectable { @override diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart index 36e3568684..3f6927df4c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart +++ b/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/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/default_selectable.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/style/plugin_styles.dart'; import 'package:appflowy_editor/src/service/render_plugin_service.dart'; import 'package:flutter/material.dart'; import 'package:appflowy_editor/src/extensions/text_style_extension.dart'; +import 'package:appflowy_editor/src/extensions/theme_extension.dart'; class BulletedListTextNodeWidgetBuilder extends NodeWidgetBuilder { @override @@ -45,11 +46,7 @@ class BulletedListTextNodeWidget extends BuiltInTextWidget { // customize class _BulletedListTextNodeWidgetState extends State - with - SelectableMixin, - DefaultSelectable, - BuiltInStyleMixin, - BuiltInTextWidgetMixin { + with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin { @override final iconKey = GlobalKey(); @@ -64,6 +61,25 @@ class _BulletedListTextNodeWidgetState extends State return super.baseOffset.translate(0, padding.top); } + BulletedListPluginStyle get style => + Theme.of(context).extensionOrNull() ?? + 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 Widget buildWithSingle(BuildContext context) { return Padding( @@ -71,12 +87,9 @@ class _BulletedListTextNodeWidgetState extends State child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - FlowySvg( + Container( key: iconKey, - width: iconSize?.width, - height: iconSize?.height, - padding: iconPadding, - name: 'point', + child: icon, ), Flexible( child: FlowyRichText( @@ -86,7 +99,7 @@ class _BulletedListTextNodeWidgetState extends State textSpan.updateTextStyle(textStyle), placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle), - lineHeight: widget.editorState.editorStyle.textStyle.lineHeight, + lineHeight: widget.editorState.editorStyle.lineHeight, textNode: widget.textNode, editorState: widget.editorState, ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart index de12388937..a2e0aa5d32 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart +++ b/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/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/extensions/text_style_extension.dart'; import 'package:flutter/material.dart'; +import 'package:appflowy_editor/src/extensions/theme_extension.dart'; class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder { @override @@ -39,11 +39,7 @@ class CheckboxNodeWidget extends BuiltInTextWidget { } class _CheckboxNodeWidgetState extends State - with - SelectableMixin, - DefaultSelectable, - BuiltInStyleMixin, - BuiltInTextWidgetMixin { + with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin { @override final iconKey = GlobalKey(); @@ -58,6 +54,25 @@ class _CheckboxNodeWidgetState extends State return super.baseOffset.translate(0, padding.top); } + CheckboxPluginStyle get style => + Theme.of(context).extensionOrNull() ?? + 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 Widget buildWithSingle(BuildContext context) { final check = widget.textNode.attributes.check; @@ -68,12 +83,7 @@ class _CheckboxNodeWidgetState extends State children: [ GestureDetector( key: iconKey, - child: FlowySvg( - width: iconSize?.width, - height: iconSize?.height, - padding: iconPadding, - name: check ? 'check' : 'uncheck', - ), + child: icon, onTap: () async { await widget.editorState.formatTextToCheckbox( widget.editorState, @@ -86,7 +96,7 @@ class _CheckboxNodeWidgetState extends State child: FlowyRichText( key: _richTextKey, placeholderText: 'To-do', - lineHeight: widget.editorState.editorStyle.textStyle.lineHeight, + lineHeight: widget.editorState.editorStyle.lineHeight, textNode: widget.textNode, textSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 40e0cffb26..8d96d143cf 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -202,12 +202,13 @@ class _FlowyRichTextState extends State with SelectableMixin { } TextSpan get _placeholderTextSpan { - final style = widget.editorState.editorStyle.textStyle; + final placeholderTextStyle = + widget.editorState.editorStyle.placeholderTextStyle; return TextSpan( children: [ TextSpan( text: widget.placeholderText, - style: style.defaultPlaceholderTextStyle, + style: placeholderTextStyle, ), ], ); @@ -216,10 +217,10 @@ class _FlowyRichTextState extends State with SelectableMixin { TextSpan get _textSpan { var offset = 0; List textSpans = []; - final style = widget.editorState.editorStyle.textStyle; + final style = widget.editorState.editorStyle; final textInserts = widget.textNode.delta.whereType(); for (final textInsert in textInserts) { - var textStyle = style.defaultTextStyle; + var textStyle = style.textStyle!; GestureRecognizer? recognizer; final attributes = textInsert.attributes; if (attributes != null) { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart index 5b6c75cc6a..f8f5bd0f92 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart +++ b/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/flowy_rich_text.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:flutter/material.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/theme_extension.dart'; class HeadingTextNodeWidgetBuilder extends NodeWidgetBuilder { @override @@ -43,7 +45,7 @@ class HeadingTextNodeWidget extends BuiltInTextWidget { // customize class _HeadingTextNodeWidgetState extends State - with SelectableMixin, DefaultSelectable, BuiltInStyleMixin { + with SelectableMixin, DefaultSelectable { @override GlobalKey? get iconKey => null; @@ -58,6 +60,20 @@ class _HeadingTextNodeWidgetState extends State return padding.topLeft; } + HeadingPluginStyle get style => + Theme.of(context).extensionOrNull() ?? + HeadingPluginStyle.light; + + EdgeInsets get padding => style.padding( + widget.editorState, + widget.textNode, + ); + + TextStyle get textStyle => style.textStyle( + widget.editorState, + widget.textNode, + ); + @override Widget build(BuildContext context) { return Padding( @@ -68,7 +84,7 @@ class _HeadingTextNodeWidgetState extends State placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle), textSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle), - lineHeight: widget.editorState.editorStyle.textStyle.lineHeight, + lineHeight: widget.editorState.editorStyle.lineHeight, textNode: widget.textNode, editorState: widget.editorState, ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart index 6ce4bd0fee..60698d6aad 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart +++ b/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/flowy_rich_text.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:flutter/material.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/theme_extension.dart'; class NumberListTextNodeWidgetBuilder extends NodeWidgetBuilder { @override @@ -43,7 +45,7 @@ class NumberListTextNodeWidget extends BuiltInTextWidget { } class _NumberListTextNodeWidgetState extends State - with SelectableMixin, DefaultSelectable, BuiltInStyleMixin { + with SelectableMixin, DefaultSelectable { @override final iconKey = GlobalKey(); @@ -58,6 +60,25 @@ class _NumberListTextNodeWidgetState extends State return super.baseOffset.translate(0, padding.top); } + NumberListPluginStyle get style => + Theme.of(context).extensionOrNull() ?? + 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 Widget build(BuildContext context) { return Padding( @@ -67,12 +88,7 @@ class _NumberListTextNodeWidgetState extends State children: [ Container( 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( child: FlowyRichText( @@ -80,7 +96,7 @@ class _NumberListTextNodeWidgetState extends State placeholderText: 'List', textNode: widget.textNode, editorState: widget.editorState, - lineHeight: widget.editorState.editorStyle.textStyle.lineHeight, + lineHeight: widget.editorState.editorStyle.lineHeight, placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle), textSpanDecorator: (textSpan) => diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart index b68fc38923..370d328d1e 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart +++ b/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/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/default_selectable.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/style/plugin_styles.dart'; import 'package:appflowy_editor/src/service/render_plugin_service.dart'; import 'package:flutter/material.dart'; import 'package:appflowy_editor/src/extensions/text_style_extension.dart'; +import 'package:appflowy_editor/src/extensions/theme_extension.dart'; class QuotedTextNodeWidgetBuilder extends NodeWidgetBuilder { @override @@ -44,7 +45,7 @@ class QuotedTextNodeWidget extends BuiltInTextWidget { // customize class _QuotedTextNodeWidgetState extends State - with SelectableMixin, DefaultSelectable, BuiltInStyleMixin { + with SelectableMixin, DefaultSelectable { @override final iconKey = GlobalKey(); @@ -59,6 +60,25 @@ class _QuotedTextNodeWidgetState extends State return super.baseOffset.translate(0, padding.top); } + QuotedTextPluginStyle get style => + Theme.of(context).extensionOrNull() ?? + 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 Widget build(BuildContext context) { return Padding( @@ -67,11 +87,9 @@ class _QuotedTextNodeWidgetState extends State child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - FlowySvg( + Container( key: iconKey, - width: iconSize?.width, - padding: iconPadding, - name: 'quote', + child: icon, ), Flexible( child: FlowyRichText( @@ -82,7 +100,7 @@ class _QuotedTextNodeWidgetState extends State textSpan.updateTextStyle(textStyle), placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle), - lineHeight: widget.editorState.editorStyle.textStyle.lineHeight, + lineHeight: widget.editorState.editorStyle.lineHeight, editorState: widget.editorState, ), ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart index a28270bf7c..f48714045b 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart +++ b/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/flowy_rich_text.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:flutter/material.dart'; import 'package:appflowy_editor/src/extensions/text_style_extension.dart'; @@ -43,11 +44,7 @@ class RichTextNodeWidget extends BuiltInTextWidget { // customize class _RichTextNodeWidgetState extends State - with - SelectableMixin, - DefaultSelectable, - BuiltInStyleMixin, - BuiltInTextWidgetMixin { + with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin { @override GlobalKey? get iconKey => null; @@ -59,20 +56,26 @@ class _RichTextNodeWidgetState extends State @override 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 Widget buildWithSingle(BuildContext context) { return Padding( - padding: padding, + padding: textPadding, child: FlowyRichText( key: _richTextKey, textNode: widget.textNode, textSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle), placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle), - lineHeight: widget.editorState.editorStyle.textStyle.lineHeight, + lineHeight: widget.editorState.editorStyle.lineHeight, editorState: widget.editorState, ), ); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart index a4322c59fe..912d9447ff 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart +++ b/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:flutter/material.dart'; -class SelectionMenuItemWidget extends StatelessWidget { +class SelectionMenuItemWidget extends StatefulWidget { const SelectionMenuItemWidget({ Key? key, required this.editorState, @@ -11,7 +11,6 @@ class SelectionMenuItemWidget extends StatelessWidget { required this.item, required this.isSelected, this.width = 140.0, - this.selectedColor = const Color(0xFFE0F8FF), }) : super(key: key); final EditorState editorState; @@ -19,33 +18,52 @@ class SelectionMenuItemWidget extends StatelessWidget { final SelectionMenuItem item; final double width; final bool isSelected; - final Color selectedColor; + + @override + State createState() => + _SelectionMenuItemWidgetState(); +} + +class _SelectionMenuItemWidgetState extends State { + var _onHover = false; @override Widget build(BuildContext context) { + final editorStyle = widget.editorState.editorStyle; return Container( padding: const EdgeInsets.fromLTRB(8.0, 5.0, 8.0, 5.0), child: SizedBox( - width: width, + width: widget.width, child: TextButton.icon( - icon: item.icon, + icon: widget.item + .icon(widget.editorState, widget.isSelected || _onHover), style: ButtonStyle( 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), ), label: Text( - item.name(), + widget.item.name(), 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: () { - item.handler(editorState, menuService, context); + widget.item + .handler(widget.editorState, widget.menuService, context); + }, + onHover: (value) { + setState(() { + _onHover = value; + }); }, ), ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart index c36b9adb8e..76e12a5f62 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart +++ b/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. const menuHeight = 200.0; const menuOffset = Offset(10, 10); - final baseOffset = + final editorOffset = 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; _selectionMenuEntry = OverlayEntry(builder: (context) { return Positioned( - top: offset.dy, + top: showBelow ? offset.dy : null, + bottom: showBelow ? null : editorHeight - offset.dy, left: offset.dx, child: SelectionMenuWidget( items: [ @@ -131,7 +139,8 @@ List get defaultSelectionMenuItems => final List _defaultSelectionMenuItems = [ SelectionMenuItem( name: () => AppFlowyEditorLocalizations.current.text, - icon: _selectionMenuIcon('text'), + icon: (editorState, onSelected) => + _selectionMenuIcon('text', editorState, onSelected), keywords: ['text'], handler: (editorState, _, __) { insertTextNodeAfterSelection(editorState, {}); @@ -139,7 +148,8 @@ final List _defaultSelectionMenuItems = [ ), SelectionMenuItem( name: () => AppFlowyEditorLocalizations.current.heading1, - icon: _selectionMenuIcon('h1'), + icon: (editorState, onSelected) => + _selectionMenuIcon('h1', editorState, onSelected), keywords: ['heading 1, h1'], handler: (editorState, _, __) { insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h1); @@ -147,7 +157,8 @@ final List _defaultSelectionMenuItems = [ ), SelectionMenuItem( name: () => AppFlowyEditorLocalizations.current.heading2, - icon: _selectionMenuIcon('h2'), + icon: (editorState, onSelected) => + _selectionMenuIcon('h2', editorState, onSelected), keywords: ['heading 2, h2'], handler: (editorState, _, __) { insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h2); @@ -155,7 +166,8 @@ final List _defaultSelectionMenuItems = [ ), SelectionMenuItem( name: () => AppFlowyEditorLocalizations.current.heading3, - icon: _selectionMenuIcon('h3'), + icon: (editorState, onSelected) => + _selectionMenuIcon('h3', editorState, onSelected), keywords: ['heading 3, h3'], handler: (editorState, _, __) { insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h3); @@ -163,13 +175,15 @@ final List _defaultSelectionMenuItems = [ ), SelectionMenuItem( name: () => AppFlowyEditorLocalizations.current.image, - icon: _selectionMenuIcon('image'), + icon: (editorState, onSelected) => + _selectionMenuIcon('image', editorState, onSelected), keywords: ['image'], handler: showImageUploadMenu, ), SelectionMenuItem( name: () => AppFlowyEditorLocalizations.current.bulletedList, - icon: _selectionMenuIcon('bulleted_list'), + icon: (editorState, onSelected) => + _selectionMenuIcon('bulleted_list', editorState, onSelected), keywords: ['bulleted list', 'list', 'unordered list'], handler: (editorState, _, __) { insertBulletedListAfterSelection(editorState); @@ -177,7 +191,8 @@ final List _defaultSelectionMenuItems = [ ), SelectionMenuItem( name: () => AppFlowyEditorLocalizations.current.numberedList, - icon: _selectionMenuIcon('number'), + icon: (editorState, onSelected) => + _selectionMenuIcon('number', editorState, onSelected), keywords: ['numbered list', 'list', 'ordered list'], handler: (editorState, _, __) { insertNumberedListAfterSelection(editorState); @@ -185,7 +200,8 @@ final List _defaultSelectionMenuItems = [ ), SelectionMenuItem( name: () => AppFlowyEditorLocalizations.current.checkbox, - icon: _selectionMenuIcon('checkbox'), + icon: (editorState, onSelected) => + _selectionMenuIcon('checkbox', editorState, onSelected), keywords: ['todo list', 'list', 'checkbox list'], handler: (editorState, _, __) { insertCheckboxAfterSelection(editorState); @@ -193,7 +209,8 @@ final List _defaultSelectionMenuItems = [ ), SelectionMenuItem( name: () => AppFlowyEditorLocalizations.current.quote, - icon: _selectionMenuIcon('quote'), + icon: (editorState, onSelected) => + _selectionMenuIcon('quote', editorState, onSelected), keywords: ['quote', 'refer'], handler: (editorState, _, __) { insertQuoteAfterSelection(editorState); @@ -201,10 +218,13 @@ final List _defaultSelectionMenuItems = [ ), ]; -Widget _selectionMenuIcon(String name) { +Widget _selectionMenuIcon( + String name, EditorState editorState, bool onSelected) { return FlowySvg( name: 'selection_menu/$name', - color: Colors.black, + color: onSelected + ? editorState.editorStyle.selectionMenuItemSelectedIconColor + : editorState.editorStyle.selectionMenuItemIconColor, width: 18.0, height: 18.0, ); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart index 2f64e8a4a6..2007c172f5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart +++ b/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 Widget icon; + final Widget Function(EditorState editorState, bool onSelected) icon; /// Customizes keywords for item. /// @@ -142,7 +142,7 @@ class _SelectionMenuWidgetState extends State { onKey: _onKey, child: Container( decoration: BoxDecoration( - color: Colors.white, + color: widget.editorState.editorStyle.selectionMenuBackgroundColor, boxShadow: [ BoxShadow( blurRadius: 5, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart index a8cc9eb638..30e4465bf8 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart @@ -1,202 +1,78 @@ 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> get lightEditorStyleExtension => [ + EditorStyle.light, + ]; -typedef PluginStyler = Object Function(EditorState editorState, Node node); -typedef PluginStyle = Map; +Iterable> get darkEditorStyleExtension => [ + EditorStyle.dark, + ]; + +class EditorStyle extends ThemeExtension { + // 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; + + // 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({ required this.padding, - required this.textStyle, required this.cursorColor, required this.selectionColor, - Map 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 _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? 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 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.italic, required this.underline, required this.strikethrough, required this.href, 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? italic, TextStyle? underline, @@ -206,10 +82,25 @@ class BuiltInTextStyle { String? highlightColorHex, 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, italic: italic ?? this.italic, underline: underline ?? this.underline, @@ -222,33 +113,87 @@ class BuiltInTextStyle { } @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 lerp( + ThemeExtension? 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), + ); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/plugin_styles.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/plugin_styles.dart new file mode 100644 index 0000000000..832244a64f --- /dev/null +++ b/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> get lightPlguinStyleExtension => [ + HeadingPluginStyle.light, + CheckboxPluginStyle.light, + NumberListPluginStyle.light, + QuotedTextPluginStyle.light, + ]; + +Iterable> 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 { + 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 lerp( + ThemeExtension? 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 { + 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 lerp( + ThemeExtension? 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 { + 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 lerp( + ThemeExtension? 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 { + 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 lerp( + ThemeExtension? 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 { + 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 lerp( + ThemeExtension? 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; +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index 56008dced2..5ab7f6cc50 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -259,7 +259,7 @@ List defaultToolbarItems = [ ), handler: (editorState, context) => formatHighlight( editorState, - editorState.editorStyle.textStyle.highlightColorHex, + editorState.editorStyle.highlightColorHex!, ), ), ]; @@ -348,6 +348,7 @@ void showLinkMenu( child: Material( child: LinkMenu( linkText: linkText, + editorState: editorState, onOpenLink: () async { await safeLaunchUrl(linkText); }, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart index 71cfd030d5..4b6170620b 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart @@ -25,6 +25,7 @@ class ToolbarItemWidget extends StatelessWidget { child: MouseRegion( cursor: SystemMouseCursors.click, child: IconButton( + hoverColor: Colors.transparent, highlightColor: Colors.transparent, padding: EdgeInsets.zero, icon: item.iconBuilder(isHighlight), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/context_menu/context_menu.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/context_menu/context_menu.dart index 80fceb03d1..92b43abdbb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/context_menu/context_menu.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/context_menu/context_menu.dart @@ -30,26 +30,43 @@ class ContextMenu extends StatelessWidget { final children = []; for (var i = 0; i < items.length; i++) { for (var j = 0; j < items[i].length; j++) { + var onHover = false; 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, ), decoration: BoxDecoration( - color: Colors.white, + color: editorState.editorStyle.selectionMenuBackgroundColor, boxShadow: [ BoxShadow( blurRadius: 5, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart index dd00a3e0c9..af9c9e7380 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart +++ b/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/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/shortcut_event.dart'; 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/rich_text/bulleted_list_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/quoted_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 = { 'editor': EditorEntryWidgetBuilder(), @@ -33,15 +24,21 @@ NodeWidgetBuilders defaultBuilders = { }; class AppFlowyEditor extends StatefulWidget { - const AppFlowyEditor({ + AppFlowyEditor({ Key? key, required this.editorState, this.customBuilders = const {}, this.shortcutEvents = const [], this.selectionMenuItems = const [], 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; @@ -53,7 +50,7 @@ class AppFlowyEditor extends StatefulWidget { final List selectionMenuItems; - final EditorStyle editorStyle; + late final ThemeData themeData; final bool editable; @@ -65,13 +62,15 @@ class _AppFlowyEditorState extends State { Widget? services; EditorState get editorState => widget.editorState; + EditorStyle get editorStyle => + editorState.themeData.extension() ?? EditorStyle.light; @override void initState() { super.initState(); editorState.selectionMenuItems = widget.selectionMenuItems; - editorState.editorStyle = widget.editorStyle; + editorState.themeData = widget.themeData; editorState.service.renderPluginService = _createRenderPlugin(); editorState.editable = widget.editable; } @@ -85,7 +84,7 @@ class _AppFlowyEditorState extends State { editorState.service.renderPluginService = _createRenderPlugin(); } - editorState.editorStyle = widget.editorStyle; + editorState.themeData = widget.themeData; editorState.editable = widget.editable; services = null; } @@ -102,38 +101,41 @@ class _AppFlowyEditorState extends State { ); } - 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, editable: widget.editable, - child: AppFlowyKeyboard( - key: editorState.service.keyboardServiceKey, - editable: widget.editable, - shortcutEvents: [ - ...widget.shortcutEvents, - ...builtInShortcutEvents, - ], + child: AppFlowyInput( + key: editorState.service.inputServiceKey, 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, - 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, + ), ), ), ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index 25f52c6914..9ab74cdb14 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/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'); RichClipboard.setData(RichClipboardData( html: htmlString, - text: textNode.toPlainText(), + text: textNode.toPlainText().substring( + selection.startIndex, + selection.endIndex, + ), )); } else { Log.keyboard.debug('unimplemented: copy non-text'); @@ -63,9 +66,19 @@ void _handleCopy(EditorState editorState) async { startOffset: selection.start.offset, endOffset: selection.end.offset, ).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)); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/format_style_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/format_style_handler.dart index 47e2dc10ab..1535efdb94 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/format_style_handler.dart +++ b/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( editorState, - editorState.editorStyle.textStyle.highlightColorHex, + editorState.editorStyle.highlightColorHex!, ); return KeyEventResult.handled; }; diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart index 6396e47ebe..d371895487 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart @@ -39,7 +39,6 @@ class EditorWidgetTester { home: Scaffold( body: AppFlowyEditor( editorState: _editorState, - editorStyle: EditorStyle.defaultStyle(), ), ), ); diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart index a9732d8a20..201f07861a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart +++ b/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 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)); expect(rightImageRect.right, - editorRect.right - editor.editorState.editorStyle.padding.right); + editorRect.right - editor.editorState.editorStyle.padding!.right); final centerImageRect = tester.getRect(imageFinder.at(1)); expect(centerImageRect.left, (leftImageRect.left + rightImageRect.left) / 2.0); diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart index f60d1610ad..d058fc2199 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart @@ -137,7 +137,7 @@ Future _prepare(WidgetTester tester) async { ); for (final item in defaultSelectionMenuItems) { - expect(find.byWidget(item.icon), findsOneWidget); + expect(find.text(item.name()), findsOneWidget); } return Future.value(editor); diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart index b3166a46fb..0b53b43af9 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart +++ b/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) { - expect(find.byWidget(item.icon), findsOneWidget); + expect(find.text(item.name()), findsOneWidget); } await editor.updateSelection(Selection.single(path: [1], startOffset: 0));