Skip to content

Commit ccaa96c

Browse files
committed
feat: custom selection color
1 parent 86914a0 commit ccaa96c

14 files changed

Lines changed: 300 additions & 22 deletions

android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.swmansion.enriched.markdown.utils.common.FeatureFlags
1818
import com.swmansion.enriched.markdown.utils.common.MarkdownSegmentRenderer
1919
import com.swmansion.enriched.markdown.utils.common.RenderedSegment
2020
import com.swmansion.enriched.markdown.utils.common.splitASTIntoSegments
21+
import com.swmansion.enriched.markdown.utils.text.view.applyMarkdownSelectionColors
2122
import com.swmansion.enriched.markdown.utils.text.view.emitLinkLongPressEvent
2223
import com.swmansion.enriched.markdown.utils.text.view.emitLinkPressEvent
2324
import com.swmansion.enriched.markdown.views.BlockSegmentView
@@ -54,6 +55,8 @@ class EnrichedMarkdown
5455
private var maxFontSizeMultiplier: Float = 0f
5556
private var allowTrailingMargin: Boolean = false
5657
private var selectable: Boolean = true
58+
private var propSelectionColor: Int? = null
59+
private var propSelectionHandleColor: Int? = null
5760

5861
private var onLinkPressCallback: ((String) -> Unit)? = null
5962
private var onLinkLongPressCallback: ((String) -> Unit)? = null
@@ -128,6 +131,22 @@ class EnrichedMarkdown
128131
}
129132
}
130133

134+
fun setSelectionColorFromProps(color: Int?) {
135+
propSelectionColor = color
136+
applySelectionColorsToSegments()
137+
}
138+
139+
fun setSelectionHandleColorFromProps(color: Int?) {
140+
propSelectionHandleColor = color
141+
applySelectionColorsToSegments()
142+
}
143+
144+
private fun applySelectionColorsToSegments() {
145+
segmentViews.filterIsInstance<EnrichedMarkdownInternalText>().forEach {
146+
it.applyMarkdownSelectionColors(propSelectionColor, propSelectionHandleColor)
147+
}
148+
}
149+
131150
fun setOnLinkPressCallback(callback: (String) -> Unit) {
132151
onLinkPressCallback = callback
133152
}
@@ -234,6 +253,8 @@ class EnrichedMarkdown
234253
if (contextMenuItemTexts.isNotEmpty()) {
235254
setContextMenuItems(contextMenuItemTexts, ::forwardContextMenuItemPress)
236255
}
256+
257+
applyMarkdownSelectionColors(propSelectionColor, propSelectionHandleColor)
237258
}
238259

239260
private fun createTableView(

android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,20 @@ class EnrichedMarkdownManager :
8080
view?.setIsSelectable(selectable)
8181
}
8282

83+
override fun setSelectionColor(
84+
view: EnrichedMarkdown?,
85+
value: Int?,
86+
) {
87+
view?.setSelectionColorFromProps(value)
88+
}
89+
90+
override fun setSelectionHandleColor(
91+
view: EnrichedMarkdown?,
92+
value: Int?,
93+
) {
94+
view?.setSelectionHandleColorFromProps(value)
95+
}
96+
8397
@ReactProp(name = "md4cFlags")
8498
override fun setMd4cFlags(
8599
view: EnrichedMarkdown?,

android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import com.swmansion.enriched.markdown.styles.StyleConfig
2222
import com.swmansion.enriched.markdown.utils.text.TailFadeInAnimator
2323
import com.swmansion.enriched.markdown.utils.text.interaction.CheckboxTouchHelper
2424
import com.swmansion.enriched.markdown.utils.text.view.LinkLongPressMovementMethod
25+
import com.swmansion.enriched.markdown.utils.text.view.applyMarkdownSelectionColors
2526
import com.swmansion.enriched.markdown.utils.text.view.applySelectableState
2627
import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForCheckboxTap
2728
import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForLinkTap
@@ -81,6 +82,9 @@ class EnrichedMarkdownText
8182
private set
8283
var spoilerOverlay: SpoilerOverlay = SpoilerOverlay.PARTICLES
8384

85+
private var propSelectionColor: Int? = null
86+
private var propSelectionHandleColor: Int? = null
87+
8488
init {
8589
setupAsMarkdownTextView()
8690
customSelectionActionModeCallback =
@@ -248,6 +252,8 @@ class EnrichedMarkdownText
248252
fadeAnimator?.animate(tailStart, styledText.length)
249253
previousTextLength = styledText.length
250254
}
255+
256+
applyMarkdownSelectionColors(propSelectionColor, propSelectionHandleColor)
251257
}
252258

253259
fun setContextMenuItems(items: List<String>) {
@@ -258,6 +264,16 @@ class EnrichedMarkdownText
258264
applySelectableState(selectable)
259265
}
260266

267+
fun setSelectionColorFromProps(color: Int?) {
268+
propSelectionColor = color
269+
applyMarkdownSelectionColors(propSelectionColor, propSelectionHandleColor)
270+
}
271+
272+
fun setSelectionHandleColorFromProps(color: Int?) {
273+
propSelectionHandleColor = color
274+
applyMarkdownSelectionColors(propSelectionColor, propSelectionHandleColor)
275+
}
276+
261277
fun emitOnLinkPress(url: String) {
262278
emitLinkPressEvent(url)
263279
}

android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,20 @@ class EnrichedMarkdownTextManager :
100100
view?.setIsSelectable(selectable)
101101
}
102102

103+
override fun setSelectionColor(
104+
view: EnrichedMarkdownText?,
105+
value: Int?,
106+
) {
107+
view?.setSelectionColorFromProps(value)
108+
}
109+
110+
override fun setSelectionHandleColor(
111+
view: EnrichedMarkdownText?,
112+
value: Int?,
113+
) {
114+
view?.setSelectionHandleColorFromProps(value)
115+
}
116+
103117
@ReactProp(name = "md4cFlags")
104118
override fun setMd4cFlags(
105119
view: EnrichedMarkdownText?,
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.swmansion.enriched.markdown.utils.text.view
2+
3+
import android.os.Build
4+
import android.widget.TextView
5+
import androidx.annotation.ColorInt
6+
import androidx.core.graphics.drawable.DrawableCompat
7+
8+
/**
9+
* Applies selection highlight and (where supported) handle tinting to a [TextView].
10+
*
11+
* Handle drawables are only tinted on API 29+ where the framework exposes getters;
12+
* on older versions the handle theme defaults remain unchanged.
13+
*/
14+
fun TextView.applyMarkdownSelectionColors(
15+
selectionColor: Int?,
16+
selectionHandleColor: Int?,
17+
) {
18+
selectionColor?.let { highlightColor = it }
19+
selectionHandleColor?.let { applySelectionHandleTint(it) }
20+
}
21+
22+
private fun TextView.applySelectionHandleTint(
23+
@ColorInt color: Int,
24+
) {
25+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
26+
return
27+
}
28+
try {
29+
tintHandle(textSelectHandleLeft, color)?.let { setTextSelectHandleLeft(it) }
30+
tintHandle(textSelectHandle, color)?.let { setTextSelectHandle(it) }
31+
tintHandle(textSelectHandleRight, color)?.let { setTextSelectHandleRight(it) }
32+
} catch (_: Exception) {
33+
// Defensive: OEM TextView variants may not support all handle accessors.
34+
}
35+
}
36+
37+
private fun tintHandle(
38+
drawable: android.graphics.drawable.Drawable?,
39+
@ColorInt color: Int,
40+
): android.graphics.drawable.Drawable? {
41+
if (drawable == null) {
42+
return null
43+
}
44+
val mutated = drawable.mutate()
45+
DrawableCompat.setTint(mutated, color)
46+
return mutated
47+
}

apps/example/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ export default function App() {
9393
onLinkPress={handleLinkPress}
9494
markdownStyle={markdownStyle}
9595
contextMenuItems={contextMenuItems}
96+
selectionColor={Platform.OS === 'ios' ? '#5A52FA' : '#DCDDFE'}
97+
selectionHandleColor="#5A52FA"
9698
/>
9799
</ScrollView>
98100
)}

ios/EnrichedMarkdown.mm

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#import "EditMenuUtils.h"
88

99
#import "ENRMFeatureFlags.h"
10+
#import "ENRMUIKit.h"
1011

1112
#if ENRICHED_MARKDOWN_MATH
1213
#import "ENRMMathContainerView.h"
@@ -90,6 +91,7 @@ + (instancetype)segmentWithLatex:(NSString *)latex
9091
#endif
9192

9293
@interface EnrichedMarkdown () <RCTEnrichedMarkdownViewProtocol, UITextViewDelegate>
94+
- (void)applySelectionTintFromProps:(const EnrichedMarkdownProps &)props toTextView:(ENRMPlatformTextView *)textView;
9395
@end
9496

9597
@implementation EnrichedMarkdown {
@@ -493,6 +495,9 @@ - (EnrichedMarkdownInternalText *)createTextViewForRenderedSegment:(ENRMRenderRe
493495
view.textView.selectable = _selectable;
494496
[view applyAttributedText:segment.attributedText context:segment.context];
495497

498+
const auto &selectionProps = *std::static_pointer_cast<EnrichedMarkdownProps const>(self->_props);
499+
[self applySelectionTintFromProps:selectionProps toTextView:view.textView];
500+
496501
ENRMTapRecognizer *tapRecognizer = [[ENRMTapRecognizer alloc] initWithTarget:self action:@selector(textTapped:)];
497502
[view.textView addGestureRecognizer:tapRecognizer];
498503

@@ -657,6 +662,18 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
657662
}
658663
}
659664

665+
if (newViewProps.selectionHandleColor != oldViewProps.selectionHandleColor ||
666+
newViewProps.selectionColor != oldViewProps.selectionColor) {
667+
#if !TARGET_OS_OSX
668+
for (RCTUIView *segment in _segmentViews) {
669+
if ([segment isKindOfClass:[EnrichedMarkdownInternalText class]]) {
670+
ENRMPlatformTextView *tv = ((EnrichedMarkdownInternalText *)segment).textView;
671+
[self applySelectionTintFromProps:newViewProps toTextView:tv];
672+
}
673+
}
674+
#endif
675+
}
676+
660677
if (markdownChanged || stylePropChanged || md4cFlagsChanged || allowTrailingMarginChanged) {
661678
NSString *markdownString = [[NSString alloc] initWithUTF8String:newViewProps.markdown.c_str()];
662679
[self renderMarkdownContent:markdownString];
@@ -880,4 +897,15 @@ - (NSInteger)indexOfAccessibilityElement:(id)element
880897
}
881898
#endif
882899

900+
- (void)applySelectionTintFromProps:(const EnrichedMarkdownProps &)props toTextView:(ENRMPlatformTextView *)textView
901+
{
902+
#if !TARGET_OS_OSX
903+
if (isColorMeaningful(props.selectionHandleColor)) {
904+
ENRMSetSelectionColor(textView, RCTUIColorFromSharedColor(props.selectionHandleColor));
905+
} else if (isColorMeaningful(props.selectionColor)) {
906+
ENRMSetSelectionColor(textView, RCTUIColorFromSharedColor(props.selectionColor));
907+
}
908+
#endif
909+
}
910+
883911
@end

ios/EnrichedMarkdownText.mm

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#import "ENRMTailFadeInAnimator.h"
1010
#import "ENRMTextRenderer.h"
1111
#import "ENRMTextViewSetup.h"
12+
#import "ENRMUIKit.h"
1213
#import "EditMenuUtils.h"
1314
#import "FontScaleObserver.h"
1415
#import "FontUtils.h"
@@ -411,6 +412,17 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
411412
_textView.selectable = newViewProps.selectable;
412413
}
413414

415+
if (newViewProps.selectionHandleColor != oldViewProps.selectionHandleColor ||
416+
newViewProps.selectionColor != oldViewProps.selectionColor) {
417+
#if !TARGET_OS_OSX
418+
if (isColorMeaningful(newViewProps.selectionHandleColor)) {
419+
ENRMSetSelectionColor(_textView, RCTUIColorFromSharedColor(newViewProps.selectionHandleColor));
420+
} else if (isColorMeaningful(newViewProps.selectionColor)) {
421+
ENRMSetSelectionColor(_textView, RCTUIColorFromSharedColor(newViewProps.selectionColor));
422+
}
423+
#endif
424+
}
425+
414426
if (newViewProps.allowFontScaling != oldViewProps.allowFontScaling) {
415427
_fontScaleObserver.allowFontScaling = newViewProps.allowFontScaling;
416428

src/EnrichedMarkdownNativeComponent.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,30 @@ export interface NativeProps extends ViewProps {
272272
* @default true
273273
*/
274274
selectable?: boolean;
275+
/**
276+
* Color of the text selection highlight (selected text background).
277+
*
278+
* - **Android**: maps to `TextView.highlightColor`.
279+
* - **iOS**: `UITextView.tintColor` drives the selection highlight, caret, and
280+
* selection handles together. When `selectionHandleColor` is also set, it
281+
* takes precedence for `tintColor` (see that prop).
282+
* - **Web**: maps to `::selection` background via a CSS variable on the root.
283+
*
284+
* @platform ios, android, web
285+
*/
286+
selectionColor?: ColorValue;
287+
/**
288+
* Color of the selection handles (drag anchors).
289+
*
290+
* - **Android**: tints the left, mid, and right handle drawables (API 29+;
291+
* older API levels leave handles at the default theme color).
292+
* - **iOS**: merged with selection via `tintColor` — when set, it overrides
293+
* `selectionColor` for the shared `tintColor` (highlight + handles + caret).
294+
* - **Web**: best-effort via `accent-color` on the root; browser support varies.
295+
*
296+
* @platform ios, android, web
297+
*/
298+
selectionHandleColor?: ColorValue;
275299
/**
276300
* MD4C parser flags configuration.
277301
* Controls how the markdown parser interprets certain syntax.

src/EnrichedMarkdownTextNativeComponent.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,30 @@ export interface NativeProps extends ViewProps {
272272
* @default true
273273
*/
274274
selectable?: boolean;
275+
/**
276+
* Color of the text selection highlight (selected text background).
277+
*
278+
* - **Android**: maps to `TextView.highlightColor`.
279+
* - **iOS**: `UITextView.tintColor` drives the selection highlight, caret, and
280+
* selection handles together. When `selectionHandleColor` is also set, it
281+
* takes precedence for `tintColor` (see that prop).
282+
* - **Web**: maps to `::selection` background via a CSS variable on the root.
283+
*
284+
* @platform ios, android, web
285+
*/
286+
selectionColor?: ColorValue;
287+
/**
288+
* Color of the selection handles (drag anchors).
289+
*
290+
* - **Android**: tints the left, mid, and right handle drawables (API 29+;
291+
* older API levels leave handles at the default theme color).
292+
* - **iOS**: merged with selection via `tintColor` — when set, it overrides
293+
* `selectionColor` for the shared `tintColor` (highlight + handles + caret).
294+
* - **Web**: best-effort via `accent-color` on the root; browser support varies.
295+
*
296+
* @platform ios, android, web
297+
*/
298+
selectionHandleColor?: ColorValue;
275299
/**
276300
* MD4C parser flags configuration.
277301
* Controls how the markdown parser interprets certain syntax.

0 commit comments

Comments
 (0)