diff --git a/super_editor/example/lib/demos/example_editor/example_editor.dart b/super_editor/example/lib/demos/example_editor/example_editor.dart index 32b7447a8b..6db9d4a28c 100644 --- a/super_editor/example/lib/demos/example_editor/example_editor.dart +++ b/super_editor/example/lib/demos/example_editor/example_editor.dart @@ -255,6 +255,16 @@ class _ExampleEditorState extends State { _editorFocusNode.requestFocus(); } + /// Makes text white, for use during dark mode styling. + /// + /// This is the same behavior observed in Apple Notes. + Color _darkModeSelectedColorStrategy({ + required Color originalTextColor, + required Color selectionHighlightColor, + }) { + return Colors.white; + } + @override Widget build(BuildContext context) { return ValueListenableBuilder( @@ -397,6 +407,7 @@ class _ExampleEditorState extends State { selectionColor: Colors.red.withOpacity(0.3), ), stylesheet: defaultStylesheet.copyWith( + selectedTextColorStrategy: isLight ? null : _darkModeSelectedColorStrategy, addRulesAfter: [ if (!isLight) ..._darkModeStyles, taskStyles, diff --git a/super_editor/lib/src/default_editor/layout_single_column/_styler_user_selection.dart b/super_editor/lib/src/default_editor/layout_single_column/_styler_user_selection.dart index f71369e99e..7b65dceb13 100644 --- a/super_editor/lib/src/default_editor/layout_single_column/_styler_user_selection.dart +++ b/super_editor/lib/src/default_editor/layout_single_column/_styler_user_selection.dart @@ -132,25 +132,52 @@ class SingleColumnLayoutSelectionStyler extends SingleColumnLayoutStylePhase { editorStyleLog.finer(' - extent: ${textSelection?.extent}'); if (viewModel is TextComponentViewModel) { - final componentTextColor = viewModel.textStyleBuilder({}).color; - - final textWithSelectionAttributions = - textSelection != null && _selectedTextColorStrategy != null && componentTextColor != null - ? (viewModel.text.copyText(0) - ..addAttribution( - ColorAttribution(_selectedTextColorStrategy!( - originalTextColor: componentTextColor, - selectionHighlightColor: _selectionStyles.selectionColor, - )), - SpanRange(textSelection.start, textSelection.end - 1), - // The selected range might already have a color attribution. We want to override it - // with the selected text color. - overwriteConflictingSpans: true, - )) - : viewModel.text; + AttributedText? textWithSelectionAttributions; + + if (textSelection != null && _selectedTextColorStrategy != null) { + final selectedRange = SpanRange(textSelection.start, textSelection.end - 1); + + final componentTextColor = viewModel.textStyleBuilder({}).color; + if (componentTextColor != null) { + // Compute the selected text color for the default color of the node. If there is any + // text color attributions in the selected range, they will override this color. + textWithSelectionAttributions = viewModel.text.copyText(0) + ..addAttribution( + ColorAttribution(_selectedTextColorStrategy!( + originalTextColor: componentTextColor, + selectionHighlightColor: _selectionStyles.selectionColor, + )), + selectedRange, + // Override any existing color attributions. + overwriteConflictingSpans: true, + ); + } + + final coloredSpans = viewModel.text.getAttributionSpansInRange( + attributionFilter: (attr) => attr is ColorAttribution, + range: selectedRange, + // We should only change the selected portion of each span. + resizeSpansToFitInRange: true, + ); + if (coloredSpans.isNotEmpty) { + // Compute and apply the selected text color for each span that has a color attribution. + textWithSelectionAttributions ??= viewModel.text.copyText(0); + for (final span in coloredSpans) { + textWithSelectionAttributions.addAttribution( + ColorAttribution(_selectedTextColorStrategy!( + originalTextColor: (span.attribution as ColorAttribution).color, + selectionHighlightColor: _selectionStyles.selectionColor, + )), + SpanRange(span.start, span.end), + // Override any existing color attributions. + overwriteConflictingSpans: true, + ); + } + } + } viewModel - ..text = textWithSelectionAttributions + ..text = textWithSelectionAttributions ?? viewModel.text ..selection = textSelection ..selectionColor = _selectionStyles.selectionColor ..highlightWhenEmpty = highlightWhenEmpty; diff --git a/super_editor/test/super_editor/supereditor_selection_test.dart b/super_editor/test/super_editor/supereditor_selection_test.dart index 9437ea9d41..aa67d10f6c 100644 --- a/super_editor/test/super_editor/supereditor_selection_test.dart +++ b/super_editor/test/super_editor/supereditor_selection_test.dart @@ -84,6 +84,80 @@ void main() { expect(richText.getSpanForPosition(const TextPosition(offset: 5))!.style!.color, Colors.green); expect(richText.getSpanForPosition(const TextPosition(offset: 16))!.style!.color, Colors.green); }); + + testWidgetsOnArbitraryDesktop("can choose new selected text color based on the original text color", + (tester) async { + final stylesheet = defaultStylesheet.copyWith( + selectedTextColorStrategy: ({required Color originalTextColor, required Color selectionHighlightColor}) { + if (originalTextColor == Colors.green) { + return Colors.red; + } + + if (originalTextColor == Colors.yellow) { + return Colors.blue; + } + + return Colors.white; + }, + ); + + // Pump an editor with a paragraph with the following colors: + // Lorem ipsum dolor + // gggggg----------- + // ------yyyyyy----- + // ------------bbbbb (black, the default color) + await tester // + .createDocument() + .withCustomContent( + MutableDocument( + nodes: [ + ParagraphNode( + id: '1', + text: AttributedText( + 'Lorem ipsum dolor', + AttributedSpans( + attributions: [ + SpanMarker( + attribution: const ColorAttribution(Colors.green), + offset: 0, + markerType: SpanMarkerType.start), + SpanMarker( + attribution: const ColorAttribution(Colors.green), + offset: 5, + markerType: SpanMarkerType.end), + SpanMarker( + attribution: const ColorAttribution(Colors.yellow), + offset: 6, + markerType: SpanMarkerType.start), + SpanMarker( + attribution: const ColorAttribution(Colors.yellow), + offset: 11, + markerType: SpanMarkerType.end), + ], + ), + ), + ), + ], + ), + ) + .useStylesheet(stylesheet) + .pump(); + + // Triple tap to select the whole paragraph. + await tester.tripleTapInParagraph('1', 2); + + // Ensure that all spans changed colors. + final richText = SuperEditorInspector.findRichTextInParagraph('1'); + + expect(richText.getSpanForPosition(const TextPosition(offset: 0))!.style!.color, Colors.red); + expect(richText.getSpanForPosition(const TextPosition(offset: 5))!.style!.color, Colors.red); + + expect(richText.getSpanForPosition(const TextPosition(offset: 6))!.style!.color, Colors.blue); + expect(richText.getSpanForPosition(const TextPosition(offset: 11))!.style!.color, Colors.blue); + + expect(richText.getSpanForPosition(const TextPosition(offset: 12))!.style!.color, Colors.white); + expect(richText.getSpanForPosition(const TextPosition(offset: 16))!.style!.color, Colors.white); + }); }); testWidgetsOnArbitraryDesktop("calculates upstream document selection within a single node", (tester) async {