Merge pull request #1364 from LucasXu0/adapt_dart_mode

feat: adapt dark mode
This commit is contained in:
Lucas.Xu 2022-10-26 14:55:37 +08:00 committed by GitHub
commit c7c9048fe3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 921 additions and 534 deletions

View File

@ -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(

View File

@ -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 editorStyle = theme.isDark ? EditorStyle.dark : EditorStyle.light;
var textStyle = theme.isDark editorStyle = editorStyle.copyWith(
? BuiltInTextStyle.builtInDarkMode() textStyle: editorStyle.textStyle?.copyWith(
: BuiltInTextStyle.builtIn();
textStyle = textStyle.copyWith(
defaultTextStyle: textStyle.defaultTextStyle.copyWith(
fontFamily: 'poppins', 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, fontWeight: FontWeight.w500,
), ),
); );
return EditorStyle.defaultStyle().copyWith( return editorStyle;
padding: const EdgeInsets.symmetric(horizontal: 80), }
textStyle: textStyle,
pluginStyles: { Iterable<ThemeExtension<dynamic>> customPluginTheme(BuildContext context) {
'text/heading': builtInPluginStyle final theme = context.watch<AppTheme>();
..update( const basePadding = 12.0;
'textStyle', var headingPluginStyle =
(_) => (EditorState editorState, Node node) { theme.isDark ? HeadingPluginStyle.dark : HeadingPluginStyle.light;
final headingToFontSize = { headingPluginStyle = headingPluginStyle.copyWith(
'h1': baseFontSize + 12, textStyle: (EditorState editorState, Node node) {
'h2': baseFontSize + 8, final headingToFontSize = {
'h3': baseFontSize + 4, 'h1': _baseFontSize + 12,
'h4': baseFontSize, 'h2': _baseFontSize + 8,
'h5': baseFontSize, 'h3': _baseFontSize + 4,
'h6': baseFontSize, 'h4': _baseFontSize,
}; 'h5': _baseFontSize,
final fontSize = 'h6': _baseFontSize,
headingToFontSize[node.attributes.heading] ?? baseFontSize; };
return TextStyle(fontSize: fontSize, fontWeight: FontWeight.w600); final fontSize =
}, headingToFontSize[node.attributes.heading] ?? _baseFontSize;
) return TextStyle(fontSize: fontSize, fontWeight: FontWeight.w600);
..update( },
'padding', padding: (EditorState editorState, Node node) {
(_) => (EditorState editorState, Node node) { final headingToPadding = {
final headingToPadding = { 'h1': basePadding + 6,
'h1': basePadding + 6, 'h2': basePadding + 4,
'h2': basePadding + 4, 'h3': basePadding + 2,
'h3': basePadding + 2, 'h4': basePadding,
'h4': basePadding, 'h5': basePadding,
'h5': basePadding, 'h6': basePadding,
'h6': basePadding, };
}; final padding = headingToPadding[node.attributes.heading] ?? basePadding;
final padding = return EdgeInsets.only(bottom: 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);
} }

View File

@ -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'],

View File

@ -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,
);
};
},
),
},
);
}
} }

View File

@ -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,
), ),
), ),

View File

@ -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,

View File

@ -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,

View File

@ -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';

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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,
), ),
); );

View File

@ -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( style: TextStyle(
color: Colors.black, color: style?.selectionMenuItemTextColor ?? Colors.black,
fontSize: 14.0, fontSize: 14.0,
), ),
), ),

View File

@ -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

View File

@ -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 with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
SelectableMixin,
DefaultSelectable,
BuiltInStyleMixin,
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, child: icon,
height: iconSize?.height,
padding: iconPadding,
name: 'point',
), ),
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,
), ),

View File

@ -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 with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
SelectableMixin,
DefaultSelectable,
BuiltInStyleMixin,
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( child: icon,
width: iconSize?.width,
height: iconSize?.height,
padding: iconPadding,
name: check ? 'check' : 'uncheck',
),
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),

View File

@ -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) {

View File

@ -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,
), ),

View File

@ -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: icon,
child: Text(
'${widget.textNode.attributes.number.toString()}.',
// FIXME: customize
style: const TextStyle(fontSize: 16.0, color: Colors.black),
),
), ),
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) =>

View File

@ -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, child: icon,
padding: iconPadding,
name: 'quote',
), ),
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,
), ),
), ),

View File

@ -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 with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
SelectableMixin,
DefaultSelectable,
BuiltInStyleMixin,
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,
), ),
); );

View File

@ -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), overlayColor: MaterialStateProperty.all(
backgroundColor: isSelected editorStyle.selectionMenuItemSelectedColor),
? MaterialStateProperty.all(selectedColor) 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( style: TextStyle(
color: Colors.black, color: (widget.isSelected || _onHover)
fontSize: 14.0, ? 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;
});
}, },
), ),
), ),

View File

@ -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; final editorHeight = editorState.renderBox!.size.height;
if (offset.dy >=
baseOffset.dy + editorState.renderBox!.size.height - menuHeight) { // show below defualt
offset = selectionRects.first.topRight - menuOffset; var showBelow = true;
offset = offset.translate(0, -menuHeight); 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,
); );

View File

@ -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,

View File

@ -1,202 +1,78 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/core/document/node.dart'; Iterable<ThemeExtension<dynamic>> get lightEditorStyleExtension => [
import 'package:appflowy_editor/src/editor_state.dart'; EditorStyle.light,
import 'package:appflowy_editor/src/extensions/attributes_extension.dart'; ];
typedef PluginStyler = Object Function(EditorState editorState, Node node); Iterable<ThemeExtension<dynamic>> get darkEditorStyleExtension => [
typedef PluginStyle = Map<String, PluginStyler>; 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;
// 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 {}, required this.selectionMenuBackgroundColor,
}) { required this.selectionMenuItemTextColor,
_pluginStyles.addAll(pluginStyles); required this.selectionMenuItemIconColor,
} required this.selectionMenuItemSelectedTextColor,
required this.selectionMenuItemSelectedIconColor,
EditorStyle.defaultStyle() required this.selectionMenuItemSelectedColor,
: padding = const EdgeInsets.fromLTRB(200.0, 0.0, 200.0, 0.0), required this.textPadding,
textStyle = BuiltInTextStyle.builtIn(), required this.textStyle,
cursorColor = const Color(0xFF00BCF0), required this.placeholderTextStyle,
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.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', required this.highlightColorHex,
this.lineHeight = 1.5, required this.lineHeight,
}); });
final TextStyle defaultTextStyle; @override
final TextStyle defaultPlaceholderTextStyle; EditorStyle copyWith({
final TextStyle bold; EdgeInsets? padding,
final TextStyle italic; Color? cursorColor,
final TextStyle underline; Color? selectionColor,
final TextStyle strikethrough; Color? selectionMenuBackgroundColor,
final TextStyle href; Color? selectionMenuItemTextColor,
final TextStyle code; Color? selectionMenuItemIconColor,
final String highlightColorHex; Color? selectionMenuItemSelectedTextColor,
final double lineHeight; Color? selectionMenuItemSelectedIconColor,
Color? selectionMenuItemSelectedColor,
BuiltInTextStyle.builtIn() TextStyle? textStyle,
: defaultTextStyle = const TextStyle(fontSize: 16.0, color: Colors.black), TextStyle? placeholderTextStyle,
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,
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( return EditorStyle(
defaultTextStyle: defaultTextStyle ?? this.defaultTextStyle, padding: padding ?? this.padding,
defaultPlaceholderTextStyle: cursorColor: cursorColor ?? this.cursorColor,
defaultPlaceholderTextStyle ?? this.defaultPlaceholderTextStyle, 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) { ThemeExtension<EditorStyle> lerp(
if (identical(this, other)) return true; ThemeExtension<EditorStyle>? other, double t) {
if (other == null || other is! EditorStyle) {
return other is BuiltInTextStyle && return this;
other.defaultTextStyle == defaultTextStyle && }
other.defaultPlaceholderTextStyle == defaultPlaceholderTextStyle && return EditorStyle(
other.bold == bold && padding: EdgeInsets.lerp(padding, other.padding, t),
other.italic == italic && cursorColor: Color.lerp(cursorColor, other.cursorColor, t),
other.underline == underline && textPadding: EdgeInsets.lerp(textPadding, other.textPadding, t),
other.strikethrough == strikethrough && selectionColor: Color.lerp(selectionColor, other.selectionColor, t),
other.href == href && selectionMenuBackgroundColor: Color.lerp(
other.code == code && selectionMenuBackgroundColor, other.selectionMenuBackgroundColor, t),
other.highlightColorHex == highlightColorHex && selectionMenuItemTextColor: Color.lerp(
other.lineHeight == lineHeight; 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 static final light = EditorStyle(
int get hashCode { padding: const EdgeInsets.fromLTRB(200.0, 0.0, 200.0, 0.0),
return defaultTextStyle.hashCode ^ cursorColor: const Color(0xFF00BCF0),
defaultPlaceholderTextStyle.hashCode ^ selectionColor: const Color.fromARGB(53, 111, 201, 231),
bold.hashCode ^ selectionMenuBackgroundColor: const Color(0xFFFFFFFF),
italic.hashCode ^ selectionMenuItemTextColor: const Color(0xFF333333),
underline.hashCode ^ selectionMenuItemIconColor: const Color(0xFF333333),
strikethrough.hashCode ^ selectionMenuItemSelectedTextColor: const Color(0xFF333333),
href.hashCode ^ selectionMenuItemSelectedIconColor: const Color(0xFF333333),
code.hashCode ^ selectionMenuItemSelectedColor: const Color(0xFFE0F8FF),
highlightColorHex.hashCode ^ textPadding: const EdgeInsets.symmetric(vertical: 8.0),
lineHeight.hashCode; 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),
);
} }

View File

@ -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;
}

View File

@ -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);
}, },

View File

@ -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),

View File

@ -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( StatefulBuilder(
child: InkWell( builder: (BuildContext context, setState) {
hoverColor: const Color(0xFFE0F8FF), return Material(
customBorder: RoundedRectangleBorder( color: editorState.editorStyle.selectionMenuBackgroundColor,
borderRadius: BorderRadius.circular(6), child: InkWell(
), hoverColor:
onTap: () { editorState.editorStyle.selectionMenuItemSelectedColor,
items[i][j].onPressed(editorState); customBorder: RoundedRectangleBorder(
onPressed(); borderRadius: BorderRadius.circular(6),
}, ),
child: Padding( onTap: () {
padding: const EdgeInsets.all(8.0), items[i][j].onPressed(editorState);
child: Text( onPressed();
items[i][j].name, },
textAlign: TextAlign.start, onHover: (value) => setState(() {
style: const TextStyle(fontSize: 14), 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,

View File

@ -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, ThemeData? themeData,
}) : super(key: key); }) : 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) { Widget _buildServices(BuildContext context) {
return AppFlowyScroll( return Theme(
key: editorState.service.scrollServiceKey, data: widget.themeData,
child: Padding( child: AppFlowyScroll(
padding: widget.editorStyle.padding, key: editorState.service.scrollServiceKey,
child: AppFlowySelection( child: Padding(
key: editorState.service.selectionServiceKey, padding: editorStyle.padding!,
cursorColor: widget.editorStyle.cursorColor, child: AppFlowySelection(
selectionColor: widget.editorStyle.selectionColor, key: editorState.service.selectionServiceKey,
editorState: editorState, cursorColor: editorStyle.cursorColor!,
editable: widget.editable, selectionColor: editorStyle.selectionColor!,
child: AppFlowyInput(
key: editorState.service.inputServiceKey,
editorState: editorState, editorState: editorState,
editable: widget.editable, editable: widget.editable,
child: AppFlowyKeyboard( child: AppFlowyInput(
key: editorState.service.keyboardServiceKey, key: editorState.service.inputServiceKey,
editable: widget.editable,
shortcutEvents: [
...widget.shortcutEvents,
...builtInShortcutEvents,
],
editorState: editorState, editorState: editorState,
child: FlowyToolbar( editable: widget.editable,
key: editorState.service.toolbarServiceKey, child: AppFlowyKeyboard(
key: editorState.service.keyboardServiceKey,
editable: widget.editable,
shortcutEvents: [
...widget.shortcutEvents,
...builtInShortcutEvents,
],
editorState: editorState, editorState: editorState,
child: child: FlowyToolbar(
editorState.service.renderPluginService.buildPluginWidget( key: editorState.service.toolbarServiceKey,
NodeWidgetContext( editorState: editorState,
context: context, child:
node: editorState.document.root, editorState.service.renderPluginService.buildPluginWidget(
editorState: editorState, NodeWidgetContext(
context: context,
node: editorState.document.root,
editorState: editorState,
),
), ),
), ),
), ),

View File

@ -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 var text = '';
.map((node) => node is TextNode ? node.toPlainText() : '\n') for (final node in nodes) {
.join('\n'); 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));
} }

View File

@ -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;
}; };

View File

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

View File

@ -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);

View File

@ -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);

View File

@ -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));