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)}';
+ }
}