diff --git a/.github/workflows/appflowy_editor_test.yml b/.github/workflows/appflowy_editor_test.yml index d075513d6f..4ad3600297 100644 --- a/.github/workflows/appflowy_editor_test.yml +++ b/.github/workflows/appflowy_editor_test.yml @@ -44,7 +44,7 @@ jobs: - uses: codecov/codecov-action@v3 with: name: appflowy_editor - flags: appflowy editor + flags: appflowy_editor env_vars: ${{ matrix.os }} fail_ci_if_error: true verbose: true diff --git a/frontend/app_flowy/assets/translations/en.json b/frontend/app_flowy/assets/translations/en.json index f4766cc2b7..7e9987568e 100644 --- a/frontend/app_flowy/assets/translations/en.json +++ b/frontend/app_flowy/assets/translations/en.json @@ -96,7 +96,8 @@ "inlineCode": "Inline Code", "quote": "Quote Block", "header": "Header", - "highlight": "Highlight" + "highlight": "Highlight", + "color": "Color" }, "tooltip": { "lightMode": "Switch to Light mode", diff --git a/frontend/app_flowy/assets/translations/es-VE.json b/frontend/app_flowy/assets/translations/es-VE.json index eb51cbbc14..91a59858a9 100644 --- a/frontend/app_flowy/assets/translations/es-VE.json +++ b/frontend/app_flowy/assets/translations/es-VE.json @@ -90,7 +90,8 @@ "inlineCode": "Código embebido", "quote": "Cita", "header": "Título", - "highlight": "Resaltado" + "highlight": "Resaltado", + "color": "Color" }, "tooltip": { "lightMode": "Cambiar a modo Claro", diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/checkmark.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/checkmark.svg new file mode 100644 index 0000000000..f9c848f713 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/checkmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_en.arb b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_en.arb index f9968ab07a..4c3a2f835a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_en.arb +++ b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_en.arb @@ -16,6 +16,8 @@ "@heading3": {}, "highlight": "Highlight", "@highlight": {}, + "color": "Color", + "@color": {}, "image": "Image", "@image": {}, "italic": "Italic", @@ -31,5 +33,45 @@ "text": "Text", "@text": {}, "underline": "Underline", - "@underline": {} + "@underline": {}, + "fontColorDefault": "Default", + "@fontColorDefault": {}, + "fontColorGray": "Gray", + "@fontColorGray": {}, + "fontColorBrown": "Brown", + "@fontColorBrown": {}, + "fontColorOrange": "Orange", + "@fontColorOrange": {}, + "fontColorYellow": "Yellow", + "@fontColorYellow": {}, + "fontColorGreen": "Green", + "@fontColorGreen": {}, + "fontColorBlue": "Blue", + "@fontColorBlue": {}, + "fontColorPurple": "Purple", + "@fontColorPurple": {}, + "fontColorPink": "Pink", + "@fontColorPink": {}, + "fontColorRed": "Red", + "@fontColorRed": {}, + "backgroundColorDefault": "Default background", + "@backgroundColorDefault": {}, + "backgroundColorGray": "Gray background", + "@backgroundColorGray": {}, + "backgroundColorBrown": "Brown background", + "@backgroundColorBrown": {}, + "backgroundColorOrange": "Orange background", + "@backgroundColorOrange": {}, + "backgroundColorYellow": "Yellow background", + "@backgroundColorYellow": {}, + "backgroundColorGreen": "Green background", + "@backgroundColorGreen": {}, + "backgroundColorBlue": "Blue background", + "@backgroundColorBlue": {}, + "backgroundColorPurple": "Purple background", + "@backgroundColorPurple": {}, + "backgroundColorPink": "Pink background", + "@backgroundColorPink": {}, + "backgroundColorRed": "Red background", + "@backgroundColorRed": {} } \ No newline at end of file diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/core/legacy/built_in_attribute_keys.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/legacy/built_in_attribute_keys.dart index 8cfe822f46..3f334bdd0a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/core/legacy/built_in_attribute_keys.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/legacy/built_in_attribute_keys.dart @@ -45,6 +45,7 @@ class BuiltInAttributeKey { BuiltInAttributeKey.underline, BuiltInAttributeKey.strikethrough, BuiltInAttributeKey.backgroundColor, + BuiltInAttributeKey.color, BuiltInAttributeKey.href, BuiltInAttributeKey.code, ]; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart index 48538f8bfb..93ddeb11b9 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart @@ -34,6 +34,17 @@ extension TextNodeExtension on TextNode { return value != null; }); + bool allSatisfyFontColorInSelection(Selection selection) => + allSatisfyInSelection(selection, BuiltInAttributeKey.color, (value) { + return value != null; + }); + + bool allSatisfyBackgroundColorInSelection(Selection selection) => + allSatisfyInSelection(selection, BuiltInAttributeKey.backgroundColor, + (value) { + return value != null; + }); + bool allSatisfyBoldInSelection(Selection selection) => allSatisfyInSelection(selection, BuiltInAttributeKey.bold, (value) { return value == true; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_en.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_en.dart index 0a834ae7eb..a53c9256ad 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_en.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_en.dart @@ -22,10 +22,41 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { + "backgroundColorBlue": + MessageLookupByLibrary.simpleMessage("Blue background"), + "backgroundColorBrown": + MessageLookupByLibrary.simpleMessage("Brown background"), + "backgroundColorDefault": + MessageLookupByLibrary.simpleMessage("Default background"), + "backgroundColorGray": + MessageLookupByLibrary.simpleMessage("Gray background"), + "backgroundColorGreen": + MessageLookupByLibrary.simpleMessage("Green background"), + "backgroundColorOrange": + MessageLookupByLibrary.simpleMessage("Orange background"), + "backgroundColorPink": + MessageLookupByLibrary.simpleMessage("Pink background"), + "backgroundColorPurple": + MessageLookupByLibrary.simpleMessage("Purple background"), + "backgroundColorRed": + MessageLookupByLibrary.simpleMessage("Red background"), + "backgroundColorYellow": + MessageLookupByLibrary.simpleMessage("Yellow background"), "bold": MessageLookupByLibrary.simpleMessage("Bold"), "bulletedList": MessageLookupByLibrary.simpleMessage("Bulleted List"), "checkbox": MessageLookupByLibrary.simpleMessage("Checkbox"), + "color": MessageLookupByLibrary.simpleMessage("Color"), "embedCode": MessageLookupByLibrary.simpleMessage("Embed Code"), + "fontColorBlue": MessageLookupByLibrary.simpleMessage("Blue"), + "fontColorBrown": MessageLookupByLibrary.simpleMessage("Brown"), + "fontColorDefault": MessageLookupByLibrary.simpleMessage("Default"), + "fontColorGray": MessageLookupByLibrary.simpleMessage("Gray"), + "fontColorGreen": MessageLookupByLibrary.simpleMessage("Green"), + "fontColorOrange": MessageLookupByLibrary.simpleMessage("Orange"), + "fontColorPink": MessageLookupByLibrary.simpleMessage("Pink"), + "fontColorPurple": MessageLookupByLibrary.simpleMessage("Purple"), + "fontColorRed": MessageLookupByLibrary.simpleMessage("Red"), + "fontColorYellow": MessageLookupByLibrary.simpleMessage("Yellow"), "heading1": MessageLookupByLibrary.simpleMessage("H1"), "heading2": MessageLookupByLibrary.simpleMessage("H2"), "heading3": MessageLookupByLibrary.simpleMessage("H3"), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart index 0d464022d2..f4087fd2c8 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart @@ -131,6 +131,16 @@ class AppFlowyEditorLocalizations { ); } + /// `Color` + String get color { + return Intl.message( + 'Color', + name: 'color', + desc: '', + args: [], + ); + } + /// `Image` String get image { return Intl.message( @@ -210,6 +220,206 @@ class AppFlowyEditorLocalizations { args: [], ); } + + /// `Default` + String get fontColorDefault { + return Intl.message( + 'Default', + name: 'fontColorDefault', + desc: '', + args: [], + ); + } + + /// `Gray` + String get fontColorGray { + return Intl.message( + 'Gray', + name: 'fontColorGray', + desc: '', + args: [], + ); + } + + /// `Brown` + String get fontColorBrown { + return Intl.message( + 'Brown', + name: 'fontColorBrown', + desc: '', + args: [], + ); + } + + /// `Orange` + String get fontColorOrange { + return Intl.message( + 'Orange', + name: 'fontColorOrange', + desc: '', + args: [], + ); + } + + /// `Yellow` + String get fontColorYellow { + return Intl.message( + 'Yellow', + name: 'fontColorYellow', + desc: '', + args: [], + ); + } + + /// `Green` + String get fontColorGreen { + return Intl.message( + 'Green', + name: 'fontColorGreen', + desc: '', + args: [], + ); + } + + /// `Blue` + String get fontColorBlue { + return Intl.message( + 'Blue', + name: 'fontColorBlue', + desc: '', + args: [], + ); + } + + /// `Purple` + String get fontColorPurple { + return Intl.message( + 'Purple', + name: 'fontColorPurple', + desc: '', + args: [], + ); + } + + /// `Pink` + String get fontColorPink { + return Intl.message( + 'Pink', + name: 'fontColorPink', + desc: '', + args: [], + ); + } + + /// `Red` + String get fontColorRed { + return Intl.message( + 'Red', + name: 'fontColorRed', + desc: '', + args: [], + ); + } + + /// `Default background` + String get backgroundColorDefault { + return Intl.message( + 'Default background', + name: 'backgroundColorDefault', + desc: '', + args: [], + ); + } + + /// `Gray background` + String get backgroundColorGray { + return Intl.message( + 'Gray background', + name: 'backgroundColorGray', + desc: '', + args: [], + ); + } + + /// `Brown background` + String get backgroundColorBrown { + return Intl.message( + 'Brown background', + name: 'backgroundColorBrown', + desc: '', + args: [], + ); + } + + /// `Orange background` + String get backgroundColorOrange { + return Intl.message( + 'Orange background', + name: 'backgroundColorOrange', + desc: '', + args: [], + ); + } + + /// `Yellow background` + String get backgroundColorYellow { + return Intl.message( + 'Yellow background', + name: 'backgroundColorYellow', + desc: '', + args: [], + ); + } + + /// `Green background` + String get backgroundColorGreen { + return Intl.message( + 'Green background', + name: 'backgroundColorGreen', + desc: '', + args: [], + ); + } + + /// `Blue background` + String get backgroundColorBlue { + return Intl.message( + 'Blue background', + name: 'backgroundColorBlue', + desc: '', + args: [], + ); + } + + /// `Purple background` + String get backgroundColorPurple { + return Intl.message( + 'Purple background', + name: 'backgroundColorPurple', + desc: '', + args: [], + ); + } + + /// `Pink background` + String get backgroundColorPink { + return Intl.message( + 'Pink background', + name: 'backgroundColorPink', + desc: '', + args: [], + ); + } + + /// `Red background` + String get backgroundColorRed { + return Intl.message( + 'Red background', + name: 'backgroundColorRed', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/color_menu/color_picker.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/color_menu/color_picker.dart new file mode 100644 index 0000000000..867a7e8509 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/color_menu/color_picker.dart @@ -0,0 +1,168 @@ +import 'package:appflowy_editor/src/infra/flowy_svg.dart'; +import 'package:flutter/material.dart'; + +class ColorOption { + const ColorOption({ + required this.colorHex, + required this.name, + }); + + final String colorHex; + final String name; +} + +enum _ColorType { + font, + background, +} + +class ColorPicker extends StatefulWidget { + const ColorPicker({ + super.key, + this.selectedFontColorHex, + this.selectedBackgroundColorHex, + required this.pickerBackgroundColor, + required this.fontColorOptions, + required this.backgroundColorOptions, + required this.pickerItemHoverColor, + required this.pickerItemTextColor, + required this.onSubmittedbackgroundColorHex, + required this.onSubmittedFontColorHex, + }); + + final String? selectedFontColorHex; + final String? selectedBackgroundColorHex; + final Color pickerBackgroundColor; + final Color pickerItemHoverColor; + final Color pickerItemTextColor; + final void Function(String color) onSubmittedbackgroundColorHex; + final void Function(String color) onSubmittedFontColorHex; + + final List fontColorOptions; + final List backgroundColorOptions; + + @override + State createState() => _ColorPickerState(); +} + +class _ColorPickerState extends State { + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: widget.pickerBackgroundColor, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withOpacity(0.1), + ), + ], + borderRadius: BorderRadius.circular(6.0), + ), + height: 250, + width: 220, + padding: const EdgeInsets.fromLTRB(10, 6, 10, 6), + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // font color + _buildHeader('font color'), + // padding + const SizedBox(height: 6), + _buildColorItems( + _ColorType.font, + widget.fontColorOptions, + widget.selectedFontColorHex, + ), + // background color + const SizedBox(height: 6), + _buildHeader('background color'), + const SizedBox(height: 6), + _buildColorItems( + _ColorType.background, + widget.backgroundColorOptions, + widget.selectedBackgroundColorHex, + ), + ], + ), + ), + ), + ); + } + + Widget _buildHeader(String text) { + return Text( + text, + style: const TextStyle( + color: Colors.grey, + fontWeight: FontWeight.bold, + ), + ); + } + + Widget _buildColorItems( + _ColorType type, List options, String? selectedColor) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: options + .map((e) => _buildColorItem(type, e, e.colorHex == selectedColor)) + .toList(), + ); + } + + Widget _buildColorItem(_ColorType type, ColorOption option, bool isChecked) { + return SizedBox( + height: 36, + child: InkWell( + customBorder: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + hoverColor: widget.pickerItemHoverColor, + onTap: () { + if (type == _ColorType.font) { + widget.onSubmittedFontColorHex(option.colorHex); + } else if (type == _ColorType.background) { + widget.onSubmittedbackgroundColorHex(option.colorHex); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // padding + const SizedBox(width: 6), + // icon + SizedBox.square( + dimension: 12, + child: Container( + decoration: BoxDecoration( + color: Color(int.tryParse(option.colorHex) ?? 0xFFFFFFFF), + shape: BoxShape.circle, + ), + ), + ), + // padding + const SizedBox(width: 10), + // text + Expanded( + child: Text( + option.name, + style: + TextStyle(fontSize: 12, color: widget.pickerItemTextColor), + ), + ), + // checkbox + if (isChecked) const FlowySvg(name: 'checkmark'), + const SizedBox(width: 6), + ], + ), + ), + ); + } +} 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 8d96d143cf..528f61e5e9 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 @@ -255,6 +255,11 @@ class _FlowyRichTextState extends State with SelectableMixin { TextStyle(backgroundColor: attributes.backgroundColor), ); } + if (attributes.color != null) { + textStyle = textStyle.combine( + TextStyle(color: attributes.color), + ); + } } offset += textInsert.length; textSpans.add( 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 28c0fdeafb..6cdf729b11 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 @@ -4,6 +4,7 @@ import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart'; import 'package:appflowy_editor/src/flutter/overlay.dart'; import 'package:appflowy_editor/src/infra/clipboard.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; +import 'package:appflowy_editor/src/render/color_menu/color_picker.dart'; import 'package:appflowy_editor/src/render/link_menu/link_menu.dart'; import 'package:appflowy_editor/src/extensions/text_node_extensions.dart'; import 'package:appflowy_editor/src/extensions/editor_state_extensions.dart'; @@ -264,6 +265,37 @@ List defaultToolbarItems = [ editorState.editorStyle.highlightColorHex!, ), ), + ToolbarItem( + id: 'appflowy.toolbar.color', + type: 4, + tooltipsMessage: AppFlowyEditorLocalizations.current.color, + iconBuilder: (isHighlight) => Icon( + Icons.color_lens_outlined, + size: 14, + color: isHighlight ? Colors.lightBlue : Colors.white, + ), + validator: _showInBuiltInTextSelection, + highlightCallback: (editorState) => + _allSatisfy( + editorState, + BuiltInAttributeKey.color, + (value) => + value != null && + value != _generateFontColorOptions(editorState).first.colorHex, + ) || + _allSatisfy( + editorState, + BuiltInAttributeKey.backgroundColor, + (value) => + value != null && + value != + _generateBackgroundColorOptions(editorState).first.colorHex, + ), + handler: (editorState, context) => showColorMenu( + context, + editorState, + ), + ), ]; ToolbarItemValidator _onlyShowInSingleTextSelection = (editorState) { @@ -301,6 +333,8 @@ bool _allSatisfy( } OverlayEntry? _linkMenuOverlay; +OverlayEntry? _colorMenuOverlay; + EditorState? _editorState; bool _changeSelectionInner = false; void showLinkMenu( @@ -343,6 +377,7 @@ void showLinkMenu( BuiltInAttributeKey.href, ); } + _linkMenuOverlay = OverlayEntry(builder: (context) { return Positioned( top: matchRect.bottom + 5.0, @@ -360,6 +395,7 @@ void showLinkMenu( text, textNode: textNode, ); + _dismissLinkMenu(); }, onCopyLink: () { @@ -419,3 +455,211 @@ void _dismissLinkMenu() { .removeListener(_dismissLinkMenu); _editorState = null; } + +void _dismissColorMenu() { + // workaround: SelectionService has been released after hot reload. + final isSelectionDisposed = + _editorState?.service.selectionServiceKey.currentState == null; + if (isSelectionDisposed) { + return; + } + if (_editorState?.service.selectionService.currentSelection.value == null) { + return; + } + if (_changeSelectionInner) { + _changeSelectionInner = false; + return; + } + _colorMenuOverlay?.remove(); + _colorMenuOverlay = null; + + _editorState?.service.scrollService?.enable(); + _editorState?.service.keyboardService?.enable(); + _editorState?.service.selectionService.currentSelection + .removeListener(_dismissColorMenu); + _editorState = null; +} + +void showColorMenu( + BuildContext context, + EditorState editorState, { + Selection? customSelection, +}) { + final rects = editorState.service.selectionService.selectionRects; + var maxBottom = 0.0; + late Rect matchRect; + for (final rect in rects) { + if (rect.bottom > maxBottom) { + maxBottom = rect.bottom; + matchRect = rect; + } + } + final baseOffset = + editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; + matchRect = matchRect.shift(-baseOffset); + + _dismissColorMenu(); + _editorState = editorState; + + // Since the link menu will only show in single text selection, + // We get the text node directly instead of judging details again. + final selection = customSelection ?? + editorState.service.selectionService.currentSelection.value; + + final node = editorState.service.selectionService.currentSelectedNodes; + if (selection == null || node.isEmpty || node.first is! TextNode) { + return; + } + final textNode = node.first as TextNode; + + String? backgroundColorHex; + if (textNode.allSatisfyBackgroundColorInSelection(selection)) { + backgroundColorHex = textNode.getAttributeInSelection( + selection, + BuiltInAttributeKey.backgroundColor, + ); + } + String? fontColorHex; + if (textNode.allSatisfyFontColorInSelection(selection)) { + fontColorHex = textNode.getAttributeInSelection( + selection, + BuiltInAttributeKey.color, + ); + } else { + fontColorHex = editorState.editorStyle.textStyle?.color?.toHex(); + } + + final style = editorState.editorStyle; + _colorMenuOverlay = OverlayEntry(builder: (context) { + return Positioned( + top: matchRect.bottom + 5.0, + left: matchRect.left + 10, + child: Material( + color: Colors.transparent, + child: ColorPicker( + pickerBackgroundColor: + style.selectionMenuBackgroundColor ?? Colors.white, + pickerItemHoverColor: style.selectionMenuItemSelectedColor ?? + Colors.blue.withOpacity(0.3), + pickerItemTextColor: style.selectionMenuItemTextColor ?? Colors.black, + selectedFontColorHex: fontColorHex, + selectedBackgroundColorHex: backgroundColorHex, + fontColorOptions: _generateFontColorOptions(editorState), + backgroundColorOptions: _generateBackgroundColorOptions(editorState), + onSubmittedbackgroundColorHex: (color) { + formatHighlightColor( + editorState, + color, + ); + _dismissColorMenu(); + }, + onSubmittedFontColorHex: (color) { + formatFontColor( + editorState, + color, + ); + _dismissColorMenu(); + }, + ), + ), + ); + }); + Overlay.of(context)?.insert(_colorMenuOverlay!); + + editorState.service.scrollService?.disable(); + editorState.service.keyboardService?.disable(); + editorState.service.selectionService.currentSelection + .addListener(_dismissColorMenu); +} + +List _generateFontColorOptions(EditorState editorState) { + final defaultColor = + editorState.editorStyle.textStyle?.color ?? Colors.black; // black + return [ + ColorOption( + colorHex: defaultColor.toHex(), + name: AppFlowyEditorLocalizations.current.fontColorDefault, + ), + ColorOption( + colorHex: Colors.grey.toHex(), + name: AppFlowyEditorLocalizations.current.fontColorGray, + ), + ColorOption( + colorHex: Colors.brown.toHex(), + name: AppFlowyEditorLocalizations.current.fontColorBrown, + ), + ColorOption( + colorHex: Colors.yellow.toHex(), + name: AppFlowyEditorLocalizations.current.fontColorYellow, + ), + ColorOption( + colorHex: Colors.green.toHex(), + name: AppFlowyEditorLocalizations.current.fontColorGreen, + ), + ColorOption( + colorHex: Colors.blue.toHex(), + name: AppFlowyEditorLocalizations.current.fontColorBlue, + ), + ColorOption( + colorHex: Colors.purple.toHex(), + name: AppFlowyEditorLocalizations.current.fontColorPurple, + ), + ColorOption( + colorHex: Colors.pink.toHex(), + name: AppFlowyEditorLocalizations.current.fontColorPink, + ), + ColorOption( + colorHex: Colors.red.toHex(), + name: AppFlowyEditorLocalizations.current.fontColorRed, + ), + ]; +} + +List _generateBackgroundColorOptions(EditorState editorState) { + final defaultBackgroundColorHex = + editorState.editorStyle.highlightColorHex ?? '0x6000BCF0'; + return [ + ColorOption( + colorHex: defaultBackgroundColorHex, + name: AppFlowyEditorLocalizations.current.backgroundColorDefault, + ), + ColorOption( + colorHex: Colors.grey.withOpacity(0.3).toHex(), + name: AppFlowyEditorLocalizations.current.backgroundColorGray, + ), + ColorOption( + colorHex: Colors.brown.withOpacity(0.3).toHex(), + name: AppFlowyEditorLocalizations.current.backgroundColorBrown, + ), + ColorOption( + colorHex: Colors.yellow.withOpacity(0.3).toHex(), + name: AppFlowyEditorLocalizations.current.backgroundColorYellow, + ), + ColorOption( + colorHex: Colors.green.withOpacity(0.3).toHex(), + name: AppFlowyEditorLocalizations.current.backgroundColorGreen, + ), + ColorOption( + colorHex: Colors.blue.withOpacity(0.3).toHex(), + name: AppFlowyEditorLocalizations.current.backgroundColorBlue, + ), + ColorOption( + colorHex: Colors.purple.withOpacity(0.3).toHex(), + name: AppFlowyEditorLocalizations.current.backgroundColorPurple, + ), + ColorOption( + colorHex: Colors.pink.withOpacity(0.3).toHex(), + name: AppFlowyEditorLocalizations.current.backgroundColorPink, + ), + ColorOption( + colorHex: Colors.red.withOpacity(0.3).toHex(), + name: AppFlowyEditorLocalizations.current.backgroundColorRed, + ), + ]; +} + +extension on Color { + String toHex() { + return '0x${value.toRadixString(16)}'; + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart index f0312bdeff..adb0e7db3d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart @@ -173,6 +173,22 @@ bool formatHighlight(EditorState editorState, String colorHex) { ); } +bool formatHighlightColor(EditorState editorState, String colorHex) { + return formatRichTextPartialStyle( + editorState, + BuiltInAttributeKey.backgroundColor, + customValue: colorHex, + ); +} + +bool formatFontColor(EditorState editorState, String colorHex) { + return formatRichTextPartialStyle( + editorState, + BuiltInAttributeKey.color, + customValue: colorHex, + ); +} + bool formatRichTextPartialStyle(EditorState editorState, String styleKey, {Object? customValue}) { Attributes attributes = { diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart index c20748779a..367e7702c6 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart @@ -50,14 +50,15 @@ void main() async { null, delta: Delta() ..insert( - 'appflowy.io', + link, attributes: { BuiltInAttributeKey.href: link, }, ), ); await editor.startTesting(); - final finder = find.byType(RichText); + await tester.pumpAndSettle(); + final finder = find.text(link, findRichText: true); expect(finder, findsOneWidget); // tap the link diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/toolbar_rich_text_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/toolbar_rich_text_test.dart index c9d9ef9e70..b9e774b351 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/toolbar_rich_text_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/toolbar_rich_text_test.dart @@ -2,6 +2,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/extensions/text_node_extensions.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../infra/test_editor.dart'; @@ -327,4 +328,51 @@ void main() async { ); }); })); + + group('toolbar, color picker', (() { + testWidgets( + 'Select Text, Click Toolbar and set color for the selected text', + (tester) async { + final editor = tester.editor..insertTextNode(singleLineText); + await editor.startTesting(); + + final node = editor.nodeAtPath([0]) as TextNode; + final selection = Selection( + start: Position(path: [0], offset: 0), + end: Position(path: [0], offset: singleLineText.length), + ); + + await editor.updateSelection(selection); + expect(find.byType(ToolbarWidget), findsOneWidget); + final colorButton = find.byWidgetPredicate((widget) { + if (widget is ToolbarItemWidget) { + return widget.item.id == 'appflowy.toolbar.color'; + } + return false; + }); + expect(colorButton, findsOneWidget); + await tester.tap(colorButton); + await tester.pumpAndSettle(); + // select a yellow color + final yellowButton = find.text('Yellow'); + await tester.tap(yellowButton); + await tester.pumpAndSettle(); + expect( + node.allSatisfyInSelection( + selection, + BuiltInAttributeKey.color, + (value) { + return value == Colors.yellow.toHex(); + }, + ), + true, + ); + }); + })); +} + +extension on Color { + String toHex() { + return '0x${value.toRadixString(16)}'; + } }