Skip to content

Commit 6e84c21

Browse files
authored
[iOS & Android] Enable borderRadius in all mention types (#720)
Co-authored-by: war-in <[email protected]>
1 parent cbdac4e commit 6e84c21

18 files changed

+401
-70
lines changed

WebExample/__tests__/styles.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,15 @@ test.describe('markdown content styling', () => {
4040
});
4141

4242
test('mention-here', async ({page}) => {
43-
await testMarkdownContentStyle({testContent: 'here', style: 'color: green; background-color: lime;', page});
43+
await testMarkdownContentStyle({testContent: 'here', style: 'color: green; background-color: lime; border-radius: 5px;', page});
4444
});
4545

4646
test('mention-user', async ({page}) => {
47-
await testMarkdownContentStyle({testContent: '[email protected]', style: 'color: blue; background-color: cyan;', page});
47+
await testMarkdownContentStyle({testContent: '[email protected]', style: 'color: blue; background-color: cyan; border-radius: 5px;', page});
4848
});
4949

5050
test('mention-report', async ({page}) => {
51-
await testMarkdownContentStyle({testContent: 'mention-report', style: 'color: red; background-color: pink;', page});
51+
await testMarkdownContentStyle({testContent: 'mention-report', style: 'color: red; background-color: pink; border-radius: 5px;', page});
5252
});
5353

5454
test('blockquote', async ({page, browserName}) => {

android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,16 @@ private void applyRange(@NonNull SpannableStringBuilder ssb, @NonNull MarkdownRa
7575
break;
7676
case "mention-here":
7777
setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getMentionHereColor()), start, end);
78-
setSpan(ssb, new MarkdownBackgroundColorSpan(markdownStyle.getMentionHereBackgroundColor()), start, end);
78+
setSpan(ssb, new MarkdownBackgroundSpan(markdownStyle.getMentionHereBackgroundColor(), markdownStyle.getMentionHereBorderRadius(), start, end), start, end);
7979
break;
8080
case "mention-user":
8181
// TODO: change mention color when it mentions current user
8282
setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getMentionUserColor()), start, end);
83-
setSpan(ssb, new MarkdownBackgroundColorSpan(markdownStyle.getMentionUserBackgroundColor()), start, end);
83+
setSpan(ssb, new MarkdownBackgroundSpan(markdownStyle.getMentionUserBackgroundColor(), markdownStyle.getMentionUserBorderRadius(), start, end), start, end);
8484
break;
8585
case "mention-report":
8686
setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getMentionReportColor()), start, end);
87-
setSpan(ssb, new MarkdownBackgroundColorSpan(markdownStyle.getMentionReportBackgroundColor()), start, end);
87+
setSpan(ssb, new MarkdownBackgroundSpan(markdownStyle.getMentionReportBackgroundColor(), markdownStyle.getMentionReportBorderRadius(), start, end), start, end);
8888
break;
8989
case "syntax":
9090
setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getSyntaxColor()), start, end);

android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,19 +63,27 @@ public class MarkdownStyle {
6363
@ColorInt
6464
private final int mMentionHereBackgroundColor;
6565

66+
private final float mMentionHereBorderRadius;
67+
6668
@ColorInt
6769
private final int mMentionUserColor;
6870

6971
@ColorInt
7072
private final int mMentionUserBackgroundColor;
7173

74+
private final float mMentionUserBorderRadius;
75+
7276
@ColorInt
7377
private final int mMentionReportColor;
7478

7579
@ColorInt
7680
private final int mMentionReportBackgroundColor;
7781

82+
private final float mMentionReportBorderRadius;
83+
7884
public MarkdownStyle(@NonNull ReadableMap map, @NonNull Context context) {
85+
float screenDensity = context.getResources().getDisplayMetrics().density;
86+
7987
mSyntaxColor = parseColor(map, "syntax", "color", context);
8088
mLinkColor = parseColor(map, "link", "color", context);
8189
mH1FontSize = parseFloat(map, "h1", "fontSize");
@@ -95,10 +103,13 @@ public MarkdownStyle(@NonNull ReadableMap map, @NonNull Context context) {
95103
mPreBackgroundColor = parseColor(map, "pre", "backgroundColor", context);
96104
mMentionHereColor = parseColor(map, "mentionHere", "color", context);
97105
mMentionHereBackgroundColor = parseColor(map, "mentionHere", "backgroundColor", context);
106+
mMentionHereBorderRadius = parseFloat(map, "mentionHere", "borderRadius") * screenDensity;
98107
mMentionUserColor = parseColor(map, "mentionUser", "color", context);
99108
mMentionUserBackgroundColor = parseColor(map, "mentionUser", "backgroundColor", context);
109+
mMentionUserBorderRadius = parseFloat(map, "mentionUser", "borderRadius") * screenDensity;
100110
mMentionReportColor = parseColor(map, "mentionReport", "color", context);
101111
mMentionReportBackgroundColor = parseColor(map, "mentionReport", "backgroundColor", context);
112+
mMentionReportBorderRadius = parseFloat(map, "mentionReport", "borderRadius") * screenDensity;
102113
}
103114

104115
private static int parseColor(@NonNull ReadableMap map, @NonNull String key, @NonNull String prop, @NonNull Context context) {
@@ -213,6 +224,10 @@ public int getMentionHereBackgroundColor() {
213224
return mMentionHereBackgroundColor;
214225
}
215226

227+
public float getMentionHereBorderRadius() {
228+
return mMentionHereBorderRadius;
229+
}
230+
216231
@ColorInt
217232
public int getMentionUserColor() {
218233
return mMentionUserColor;
@@ -223,6 +238,10 @@ public int getMentionUserBackgroundColor() {
223238
return mMentionUserBackgroundColor;
224239
}
225240

241+
public float getMentionUserBorderRadius() {
242+
return mMentionUserBorderRadius;
243+
}
244+
226245
@ColorInt
227246
public int getMentionReportColor() {
228247
return mMentionReportColor;
@@ -232,4 +251,8 @@ public int getMentionReportColor() {
232251
public int getMentionReportBackgroundColor() {
233252
return mMentionReportBackgroundColor;
234253
}
254+
255+
public float getMentionReportBorderRadius() {
256+
return mMentionReportBorderRadius;
257+
}
235258
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.expensify.livemarkdown.spans;
2+
3+
import android.graphics.Canvas;
4+
import android.graphics.Paint;
5+
import android.graphics.Path;
6+
import android.graphics.RectF;
7+
import android.text.StaticLayout;
8+
import android.text.TextPaint;
9+
import android.text.style.LineBackgroundSpan;
10+
11+
import androidx.annotation.ColorInt;
12+
import androidx.annotation.NonNull;
13+
14+
public class MarkdownBackgroundSpan implements MarkdownSpan, LineBackgroundSpan {
15+
16+
private final int backgroundColor;
17+
private final int mentionStart;
18+
private final int mentionEnd;
19+
private final float borderRadius;
20+
21+
private StaticLayout layout;
22+
private Path backgroundPath;
23+
24+
public MarkdownBackgroundSpan(@ColorInt int backgroundColor, float borderRadius, int mentionStart, int mentionEnd) {
25+
this.backgroundColor = backgroundColor;
26+
this.borderRadius = borderRadius;
27+
this.mentionStart = mentionStart;
28+
this.mentionEnd = mentionEnd;
29+
this.backgroundPath = new Path();
30+
}
31+
32+
@Override
33+
public void drawBackground(
34+
@NonNull Canvas canvas,
35+
@NonNull Paint paint,
36+
int left,
37+
int right,
38+
int top,
39+
int baseline,
40+
int bottom,
41+
@NonNull CharSequence text,
42+
int start,
43+
int end,
44+
int lnum
45+
) {
46+
CharSequence lineText = text.subSequence(start, end);
47+
if (layout == null || layout.getText() != lineText || layout.getWidth() != right || layout.getLineEnd(0) != lineText.length()) {
48+
int currentLineStart = 0;
49+
int currentLineEnd = lineText.length();
50+
// Create layout for the current line only
51+
layout = StaticLayout.Builder.obtain(lineText, currentLineStart, currentLineEnd, (TextPaint) paint, right).build();
52+
53+
int relativeMentionStart = mentionStart - start;
54+
int relativeMentionEnd = mentionEnd - start;
55+
56+
boolean mentionStartsInCurrentLine = currentLineStart <= relativeMentionStart;
57+
boolean mentionEndsInCurrentLine = currentLineEnd >= relativeMentionEnd;
58+
59+
float startX = layout.getPrimaryHorizontal(mentionStartsInCurrentLine ? relativeMentionStart: currentLineStart);
60+
float endX = layout.getPrimaryHorizontal(mentionEndsInCurrentLine ? relativeMentionEnd : currentLineEnd);
61+
62+
Paint.FontMetrics fm = paint.getFontMetrics();
63+
float startY = baseline + fm.ascent;
64+
float endY = baseline + fm.descent;
65+
66+
RectF lineRect = new RectF(startX, startY, endX, endY);
67+
backgroundPath.reset();
68+
backgroundPath.addRoundRect(lineRect, createRadii(mentionStartsInCurrentLine, mentionEndsInCurrentLine), Path.Direction.CW);
69+
}
70+
71+
int originalColor = paint.getColor();
72+
paint.setColor(backgroundColor);
73+
74+
canvas.drawPath(backgroundPath, paint);
75+
76+
paint.setColor(originalColor);
77+
}
78+
79+
private float[] createRadii(boolean roundedLeft, boolean roundedRight) {
80+
float[] radii = new float[8];
81+
82+
if (roundedLeft) {
83+
radii[0] = radii[1] = borderRadius; // top-left
84+
radii[6] = radii[7] = borderRadius; // bottom-left
85+
}
86+
87+
if (roundedRight) {
88+
radii[2] = radii[3] = borderRadius; // top-right
89+
radii[4] = radii[5] = borderRadius; // bottom-right
90+
}
91+
92+
return radii;
93+
}
94+
}

apple/BlockquoteTextLayoutFragment.mm

Lines changed: 0 additions & 51 deletions
This file was deleted.

apple/MarkdownFormatter.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ NS_ASSUME_NONNULL_BEGIN
66

77
const NSAttributedStringKey RCTLiveMarkdownTextAttributeName = @"RCTLiveMarkdownText";
88

9+
const NSAttributedStringKey RCTLiveMarkdownTextBackgroundAttributeName = @"RCTLiveMarkdownTextBackground";
10+
911
const NSAttributedStringKey RCTLiveMarkdownBlockquoteDepthAttributeName = @"RCTLiveMarkdownBlockquoteDepth";
1012

1113
@interface MarkdownFormatter : NSObject

apple/MarkdownFormatter.mm

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#import "MarkdownFormatter.h"
22
#import <React/RCTFont.h>
3+
#import <RNLiveMarkdown/RCTMarkdownTextBackgroundWithRange.h>
34

45
@implementation MarkdownFormatter
56

@@ -90,15 +91,24 @@ - (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedStri
9091
[attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.codeColor range:range];
9192
[attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.codeBackgroundColor range:range];
9293
} else if (type == "mention-here") {
93-
[attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionHereColor range:range];
94-
[attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionHereBackgroundColor range:range];
94+
[self applyMentionFormatting:attributedString
95+
range:range
96+
mentionColor:markdownStyle.mentionHereColor
97+
backgroundColor:markdownStyle.mentionHereBackgroundColor
98+
borderRadius:markdownStyle.mentionHereBorderRadius];
9599
} else if (type == "mention-user") {
96100
// TODO: change mention color when it mentions current user
97-
[attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionUserColor range:range];
98-
[attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionUserBackgroundColor range:range];
101+
[self applyMentionFormatting:attributedString
102+
range:range
103+
mentionColor:markdownStyle.mentionUserColor
104+
backgroundColor:markdownStyle.mentionUserBackgroundColor
105+
borderRadius:markdownStyle.mentionUserBorderRadius];
99106
} else if (type == "mention-report") {
100-
[attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionReportColor range:range];
101-
[attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionReportBackgroundColor range:range];
107+
[self applyMentionFormatting:attributedString
108+
range:range
109+
mentionColor:markdownStyle.mentionReportColor
110+
backgroundColor:markdownStyle.mentionReportBackgroundColor
111+
borderRadius:markdownStyle.mentionReportBorderRadius];
102112
} else if (type == "link") {
103113
[attributedString addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range];
104114
[attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.linkColor range:range];
@@ -118,6 +128,26 @@ - (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedStri
118128
}
119129
}
120130

131+
- (void)applyMentionFormatting:(NSMutableAttributedString *)attributedString
132+
range:(const NSRange)range
133+
mentionColor:(UIColor *)mentionColor
134+
backgroundColor:(UIColor *)backgroundColor
135+
borderRadius:(CGFloat)borderRadius
136+
{
137+
[attributedString addAttribute:NSForegroundColorAttributeName value:mentionColor range:range];
138+
if (@available(iOS 16.0, *)) {
139+
RCTMarkdownTextBackground *textBackground = [[RCTMarkdownTextBackground alloc] init];
140+
textBackground.color = backgroundColor;
141+
textBackground.borderRadius = borderRadius;
142+
143+
[attributedString addAttribute:RCTLiveMarkdownTextBackgroundAttributeName
144+
value:textBackground
145+
range:range];
146+
} else {
147+
[attributedString addAttribute:NSBackgroundColorAttributeName value:backgroundColor range:range];
148+
}
149+
}
150+
121151
static void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText, NSRange attributedTextRange)
122152
{
123153
__block CGFloat maximumLineHeight = 0;

apple/MarkdownTextInputDecoratorComponentView.mm

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ - (void)addTextInputObservers
7777
react_native_assert([childView isKindOfClass:[RCTTextInputComponentView class]] && "Child component of MarkdownTextInputDecoratorComponentView is not an instance of RCTTextInputComponentView.");
7878
RCTTextInputComponentView *textInputComponentView = (RCTTextInputComponentView *)childView;
7979
UIView<RCTBackedTextInputViewProtocol> *backedTextInputView = [textInputComponentView valueForKey:@"_backedTextInputView"];
80-
80+
8181
_observersAdded = true;
8282

8383
if ([backedTextInputView isKindOfClass:[RCTUITextField class]]) {
@@ -100,6 +100,21 @@ - (void)addTextInputObservers
100100
// format initial value
101101
[_markdownTextFieldObserver textFieldDidChange:_textField];
102102

103+
if (@available(iOS 16.0, *)) {
104+
auto key = [](std::string s) {
105+
std::reverse(s.begin(), s.end());
106+
return @(s.c_str());
107+
};
108+
109+
NSTextContainer *textContainer = [_textField valueForKey:key("reniatnoCtxet_")];
110+
NSTextLayoutManager *textLayoutManager = [textContainer valueForKey:key("reganaMtuoyaLtxet_")];
111+
112+
_markdownTextLayoutManagerDelegate = [[MarkdownTextLayoutManagerDelegate alloc] init];
113+
_markdownTextLayoutManagerDelegate.textStorage = [_textField valueForKey:key("egarotStxet_")];
114+
_markdownTextLayoutManagerDelegate.markdownUtils = _markdownUtils;
115+
textLayoutManager.delegate = _markdownTextLayoutManagerDelegate;
116+
}
117+
103118
// TODO: register blockquotes layout manager
104119
// https://github.com/Expensify/react-native-live-markdown/issues/87
105120
} else if ([backedTextInputView isKindOfClass:[RCTUITextView class]]) {
@@ -229,7 +244,7 @@ - (void)prepareForRecycle
229244
{
230245
react_native_assert(!_observersAdded && "MarkdownTextInputDecoratorComponentView was being recycled with TextInput observers still attached");
231246
[super prepareForRecycle];
232-
247+
233248
static const auto defaultProps = std::make_shared<const MarkdownTextInputDecoratorViewProps>();
234249
_props = defaultProps;
235250
_markdownUtils = [[RCTMarkdownUtils alloc] init];
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
#import <RNLiveMarkdown/RCTMarkdownUtils.h>
2+
#import <RNLiveMarkdown/RCTMarkdownTextBackgroundWithRange.h>
23
#import <UIKit/UIKit.h>
34

45
NS_ASSUME_NONNULL_BEGIN
56

67
API_AVAILABLE(ios(15.0))
7-
@interface BlockquoteTextLayoutFragment : NSTextLayoutFragment
8+
@interface MarkdownTextLayoutFragment : NSTextLayoutFragment
89

910
@property (nonnull, atomic) RCTMarkdownUtils *markdownUtils;
1011

1112
@property NSUInteger depth;
1213

14+
@property NSMutableArray<RCTMarkdownTextBackgroundWithRange *> *mentions;
15+
1316
@end
1417

1518
NS_ASSUME_NONNULL_END

0 commit comments

Comments
 (0)