Skip to content

Commit d647823

Browse files
authored
feat(css): text-stroke support (#10399)
closes #3597 closes #3972
1 parent 93e2478 commit d647823

File tree

18 files changed

+230
-76
lines changed

18 files changed

+230
-76
lines changed

apps/toolbox/src/pages/labels.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Page, Observable, EventData } from '@nativescript/core';
1+
import { Page, Observable, EventData, Label, Color } from '@nativescript/core';
22

33
let page: Page;
44

@@ -7,4 +7,23 @@ export function navigatingTo(args: EventData) {
77
page.bindingContext = new SampleData();
88
}
99

10-
export class SampleData extends Observable {}
10+
export class SampleData extends Observable {
11+
strokeLabel: Label;
12+
13+
loadedStrokeLabel(args) {
14+
this.strokeLabel = args.object;
15+
}
16+
17+
toggleStrokeStyle() {
18+
if (this.strokeLabel.style.textStroke) {
19+
this.strokeLabel.style.color = new Color('black');
20+
this.strokeLabel.style.textStroke = null;
21+
} else {
22+
this.strokeLabel.style.color = new Color('white');
23+
this.strokeLabel.style.textStroke = {
24+
color: new Color('black'),
25+
width: { value: 2, unit: 'px' },
26+
};
27+
}
28+
}
29+
}

apps/toolbox/src/pages/labels.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
<GridLayout marginTop="10" borderWidth="1" borderColor="#efefef" height="60" paddingLeft="5">
2626
<Button text="Test Button text-overflow: initial, this should be long sentence and truncated in the middle with ellipsis." textOverflow="initial" whiteSpace="nowrap" />
2727
</GridLayout>
28+
<GridLayout marginTop="10" height="60" paddingLeft="5" tap="{{toggleStrokeStyle}}">
29+
<Label text=" text-stroke" style="text-stroke: 2px black; color: #fff; font-size: 35; font-weight: bold; font-family:Arial, Helvetica, sans-serif" loaded="{{loadedStrokeLabel}}"/>
30+
</GridLayout>
2831
<Label text="maxLines 2" fontWeight="bold" marginTop="10" />
2932
<Label
3033
text="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
812 Bytes
Binary file not shown.

packages/core/platforms/ios/src/UIView+NativeScript.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@
1010

1111
-(void)nativeScriptSetFormattedTextDecorationAndTransform:(NSDictionary*)details letterSpacing:(CGFloat)letterSpacing lineHeight:(CGFloat)lineHeight;
1212

13+
-(void)nativeScriptSetFormattedTextStroke:(CGFloat)width color:(UIColor*)color;
14+
1315
@end

packages/core/platforms/ios/src/UIView+NativeScript.m

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,19 @@ -(void)nativeScriptSetFormattedTextDecorationAndTransform:(NSDictionary*)details
130130
((UILabel*)self).attributedText = attrText;
131131
}
132132
}
133+
134+
-(void)nativeScriptSetFormattedTextStroke:(CGFloat)width color:(UIColor*)color {
135+
if (width > 0) {
136+
NSMutableAttributedString *attrText = [[NSMutableAttributedString alloc] initWithAttributedString:((UILabel*)self).attributedText];
137+
[attrText addAttribute:NSStrokeWidthAttributeName value:[NSNumber numberWithFloat:width] range:(NSRange){
138+
0,
139+
attrText.length
140+
}];
141+
[attrText addAttribute:NSStrokeColorAttributeName value:color range:(NSRange){
142+
0,
143+
attrText.length
144+
}];
145+
((UILabel*)self).attributedText = attrText;
146+
}
147+
}
133148
@end

packages/core/ui/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export { CSSHelper } from './styling/css-selector';
6868

6969
export { Switch } from './switch';
7070
export { TabView, TabViewItem } from './tab-view';
71-
export { TextBase, getTransformedText, letterSpacingProperty, textAlignmentProperty, textDecorationProperty, textTransformProperty, textShadowProperty, whiteSpaceProperty, textOverflowProperty, lineHeightProperty } from './text-base';
71+
export { TextBase, getTransformedText, letterSpacingProperty, textAlignmentProperty, textDecorationProperty, textTransformProperty, textShadowProperty, textStrokeProperty, whiteSpaceProperty, textOverflowProperty, lineHeightProperty } from './text-base';
7272
export { FormattedString } from './text-base/formatted-string';
7373
export { Span } from './text-base/span';
7474
export { TextField } from './text-field';

packages/core/ui/label/index.android.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@ import { CoreTypes } from '../../core-types';
77

88
export * from '../text-base';
99

10-
let TextView: typeof android.widget.TextView;
11-
1210
@CSSType('Label')
1311
export class Label extends TextBase implements LabelDefinition {
14-
nativeViewProtected: android.widget.TextView;
15-
nativeTextViewProtected: android.widget.TextView;
12+
nativeViewProtected: org.nativescript.widgets.StyleableTextView;
13+
nativeTextViewProtected: org.nativescript.widgets.StyleableTextView;
1614

1715
get textWrap(): boolean {
1816
return this.style.whiteSpace === 'normal';
@@ -27,11 +25,7 @@ export class Label extends TextBase implements LabelDefinition {
2725

2826
@profile
2927
public createNativeView() {
30-
if (!TextView) {
31-
TextView = android.widget.TextView;
32-
}
33-
34-
return new TextView(this._context);
28+
return new org.nativescript.widgets.StyleableTextView(this._context);
3529
}
3630

3731
public initNativeView(): void {

packages/core/ui/styling/css-shadow.ts

Lines changed: 29 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,11 @@ const LENGTH_RE = /^-?[0-9]+[a-zA-Z%]*?$/;
2626
*/
2727
const isLength = (v) => v === '0' || LENGTH_RE.test(v);
2828

29-
/**
30-
* Parse a string into ShadowCSSValues
31-
* Supports any valid css box/text shadow combination.
32-
*
33-
* inspired by https://github.com/jxnblk/css-box-shadow/blob/master/index.js (MIT License)
34-
*
35-
* @param value
36-
*/
37-
export function parseCSSShadow(value: string): ShadowCSSValues {
29+
export function parseCSSShorthand(value: string): {
30+
values: Array<CoreTypes.LengthType>;
31+
color: string;
32+
inset: boolean;
33+
} {
3834
const parts = value.trim().split(PARTS_RE);
3935
const inset = parts.includes('inset');
4036
const first = parts[0];
@@ -44,67 +40,46 @@ export function parseCSSShadow(value: string): ShadowCSSValues {
4440
return null;
4541
}
4642

47-
let colorRaw = 'black';
43+
let color = 'black';
4844
if (first && !isLength(first) && first !== 'inset') {
49-
colorRaw = first;
45+
color = first;
5046
} else if (last && !isLength(last)) {
51-
colorRaw = last;
47+
color = last;
5248
}
53-
const nums = parts
49+
const values = parts
5450
.filter((n) => n !== 'inset')
55-
.filter((n) => n !== colorRaw)
51+
.filter((n) => n !== color)
5652
.map((val) => {
5753
try {
5854
return Length.parse(val);
5955
} catch (err) {
6056
return CoreTypes.zeroLength;
6157
}
6258
});
63-
const [offsetX, offsetY, blurRadius, spreadRadius] = nums;
64-
6559
return {
6660
inset,
61+
color,
62+
values,
63+
};
64+
}
65+
/**
66+
* Parse a string into ShadowCSSValues
67+
* Supports any valid css box/text shadow combination.
68+
*
69+
* inspired by https://github.com/jxnblk/css-box-shadow/blob/master/index.js (MIT License)
70+
*
71+
* @param value
72+
*/
73+
export function parseCSSShadow(value: string): ShadowCSSValues {
74+
const data = parseCSSShorthand(value);
75+
const [offsetX, offsetY, blurRadius, spreadRadius] = data.values;
76+
77+
return {
78+
inset: data.inset,
6779
offsetX: offsetX,
6880
offsetY: offsetY,
6981
blurRadius: blurRadius,
7082
spreadRadius: spreadRadius,
71-
color: new Color(colorRaw),
83+
color: new Color(data.color),
7284
};
7385
}
74-
75-
// if (value.indexOf('rgb') > -1) {
76-
// arr = value.split(' ');
77-
// colorRaw = arr.pop();
78-
// } else {
79-
// arr = value.split(/[ ,]+/);
80-
// colorRaw = arr.pop();
81-
// }
82-
83-
// let offsetX: number;
84-
// let offsetY: number;
85-
// let blurRadius: number; // not currently in use
86-
// let spreadRadius: number; // maybe rename this to just radius
87-
// let color: Color = new Color(colorRaw);
88-
89-
// if (arr.length === 2) {
90-
// offsetX = parseFloat(arr[0]);
91-
// offsetY = parseFloat(arr[1]);
92-
// } else if (arr.length === 3) {
93-
// offsetX = parseFloat(arr[0]);
94-
// offsetY = parseFloat(arr[1]);
95-
// blurRadius = parseFloat(arr[2]);
96-
// } else if (arr.length === 4) {
97-
// offsetX = parseFloat(arr[0]);
98-
// offsetY = parseFloat(arr[1]);
99-
// blurRadius = parseFloat(arr[2]);
100-
// spreadRadius = parseFloat(arr[3]);
101-
// } else {
102-
// throw new Error('Expected 3, 4 or 5 parameters. Actual: ' + value);
103-
// }
104-
// return {
105-
// offsetX: offsetX,
106-
// offsetY: offsetY,
107-
// blurRadius: blurRadius,
108-
// spreadRadius: spreadRadius,
109-
// color: color,
110-
// };
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { parseCSSStroke } from './css-stroke';
2+
import { CoreTypes } from '../../core-types';
3+
import { Length } from './style-properties';
4+
import { Color } from '../../color';
5+
6+
describe('css-text-stroke', () => {
7+
it('empty', () => {
8+
const stroke = parseCSSStroke('');
9+
expect(stroke.width).toBe(CoreTypes.zeroLength);
10+
expect(stroke.color).toEqual(new Color('black'));
11+
});
12+
13+
it('1px navy', () => {
14+
const stroke = parseCSSStroke('1px navy');
15+
expect(stroke.width).toEqual(Length.parse('1px'));
16+
expect(stroke.color).toEqual(new Color('navy'));
17+
});
18+
19+
it('5 green', () => {
20+
const stroke = parseCSSStroke('5 green');
21+
expect(stroke.width).toEqual(Length.parse('5'));
22+
expect(stroke.color).toEqual(new Color('green'));
23+
});
24+
25+
it('2px #999', () => {
26+
const stroke = parseCSSStroke('2px #999');
27+
expect(stroke.width).toEqual(Length.parse('2px'));
28+
expect(stroke.color).toEqual(new Color('#999'));
29+
});
30+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { CoreTypes } from '../../core-types';
2+
import { Color } from '../../color';
3+
import { parseCSSShorthand } from './css-shadow';
4+
5+
export interface StrokeCSSValues {
6+
width: CoreTypes.LengthType;
7+
color: Color;
8+
}
9+
10+
/**
11+
* Parse a string into StrokeCSSValues
12+
* https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-text-stroke
13+
* @param value
14+
*/
15+
export function parseCSSStroke(value: string): StrokeCSSValues {
16+
const data = parseCSSShorthand(value);
17+
const [width] = data.values;
18+
19+
return {
20+
width,
21+
color: new Color(data.color),
22+
};
23+
}

0 commit comments

Comments
 (0)