Content-Length: 1236359 | pFad | http://github.com/NativeScript/NativeScript/commit/d6478237ec4f4d8fa4b457f39bddcc463e90643c

E5 feat(css): text-stroke support (#10399) · NativeScript/NativeScript@d647823 · GitHub
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

+21-2
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

+3
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

+2
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

+15
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

+1-1
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

+3-9
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

+29-54
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-
// };
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+
});
+23
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+
}

packages/core/ui/styling/style/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Trace } from '../../../trace';
1111
import { CoreTypes } from '../../../core-types';
1212
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState } from '../../../accessibility/accessibility-types';
1313
import { ShadowCSSValues } from '../css-shadow';
14+
import { StrokeCSSValues } from '../css-stroke';
1415

1516
export interface CommonLayoutParams {
1617
width: number;
@@ -171,6 +172,7 @@ export class Style extends Observable implements StyleDefinition {
171172
public textDecoration: CoreTypes.TextDecorationType;
172173
public textTransform: CoreTypes.TextTransformType;
173174
public textShadow: ShadowCSSValues;
175+
public textStroke: StrokeCSSValues;
174176
public whiteSpace: CoreTypes.WhiteSpaceType;
175177
public textOverflow: CoreTypes.TextOverflowType;
176178

packages/core/ui/text-base/index.android.ts

+16-6
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@ import { ShadowCSSValues } from '../styling/css-shadow';
55
// Requires
66
import { Font } from '../styling/font';
77
import { backgroundColorProperty } from '../styling/style-properties';
8-
import { TextBaseCommon, formattedTextProperty, textAlignmentProperty, textDecorationProperty, textProperty, textTransformProperty, textShadowProperty, letterSpacingProperty, whiteSpaceProperty, lineHeightProperty, isBold, resetSymbol } from './text-base-common';
8+
import { TextBaseCommon, formattedTextProperty, textAlignmentProperty, textDecorationProperty, textProperty, textTransformProperty, textShadowProperty, textStrokeProperty, letterSpacingProperty, whiteSpaceProperty, lineHeightProperty, isBold, resetSymbol } from './text-base-common';
99
import { Color } from '../../color';
1010
import { colorProperty, fontSizeProperty, fontInternalProperty, paddingLeftProperty, paddingTopProperty, paddingRightProperty, paddingBottomProperty, Length } from '../styling/style-properties';
11+
import { StrokeCSSValues } from '../styling/css-stroke';
1112
import { FormattedString } from './formatted-string';
1213
import { Span } from './span';
1314
import { CoreTypes } from '../../core-types';
1415
import { layout } from '../../utils';
1516
import { SDK_VERSION } from '../../utils/constants';
1617
import { isString, isNullOrUndefined } from '../../utils/types';
1718
import { accessibilityIdentifierProperty } from '../../accessibility/accessibility-properties';
18-
import * as Utils from '../../utils';
1919
import { testIDProperty } from '../../ui/core/view';
2020

2121
export * from './text-base-common';
@@ -169,9 +169,9 @@ function initializeBaselineAdjustedSpan(): void {
169169
}
170170

171171
export class TextBase extends TextBaseCommon {
172-
nativeViewProtected: android.widget.TextView;
172+
nativeViewProtected: org.nativescript.widgets.StyleableTextView;
173173
// @ts-ignore
174-
nativeTextViewProtected: android.widget.TextView;
174+
nativeTextViewProtected: org.nativescript.widgets.StyleableTextView;
175175
private _defaultTransformationMethod: android.text.method.TransformationMethod;
176176
private _paintFlags: number;
177177
private _minHeight: number;
@@ -237,6 +237,9 @@ export class TextBase extends TextBaseCommon {
237237

238238
this._setNativeText(reset);
239239
}
240+
[textStrokeProperty.setNative](value: StrokeCSSValues) {
241+
this._setNativeText();
242+
}
240243
createFormattedTextNative(value: FormattedString) {
241244
return createSpannableStringBuilder(value, this.style.fontSize);
242245
}
@@ -386,7 +389,7 @@ export class TextBase extends TextBaseCommon {
386389
}
387390
}
388391

389-
[textDecorationProperty.getDefault](value: number) {
392+
[textDecorationProperty.getDefault]() {
390393
return (this._paintFlags = this.nativeTextViewProtected.getPaintFlags());
391394
}
392395

@@ -410,7 +413,7 @@ export class TextBase extends TextBaseCommon {
410413
}
411414
}
412415

413-
[textShadowProperty.getDefault](value: number) {
416+
[textShadowProperty.getDefault]() {
414417
return {
415418
radius: this.nativeTextViewProtected.getShadowRadius(),
416419
offsetX: this.nativeTextViewProtected.getShadowDx(),
@@ -498,6 +501,13 @@ export class TextBase extends TextBaseCommon {
498501
transformedText = getTransformedText(stringValue, this.textTransform);
499502
}
500503

504+
if (this.style?.textStroke) {
505+
this.nativeViewProtected.setTextStroke(Length.toDevicePixels(this.style.textStroke.width), this.style.textStroke.color.android, this.style.color.android);
506+
} else if (this.nativeViewProtected.setTextStroke) {
507+
// reset
508+
this.nativeViewProtected.setTextStroke(0, 0, 0);
509+
}
510+
501511
this.nativeTextViewProtected.setText(<any>transformedText);
502512
}
503513

packages/core/ui/text-base/index.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Length } from '../styling/style-properties';
55
import { Property, CssProperty, InheritedCssProperty } from '../core/properties';
66
import { CoreTypes } from '../../core-types';
77
import { ShadowCSSValues } from '../styling/css-shadow';
8+
import { StrokeCSSValues } from '../styling/css-stroke';
89

910
export class TextBase extends View implements AddChildFromBuilder {
1011
/**
@@ -138,6 +139,7 @@ export const textAlignmentProperty: InheritedCssProperty<Style, CoreTypes.TextAl
138139
export const textDecorationProperty: CssProperty<Style, CoreTypes.TextDecorationType>;
139140
export const textTransformProperty: CssProperty<Style, CoreTypes.TextTransformType>;
140141
export const textShadowProperty: CssProperty<Style, ShadowCSSValues>;
142+
export const textStrokeProperty: CssProperty<Style, StrokeCSSValues>;
141143
export const whiteSpaceProperty: CssProperty<Style, CoreTypes.WhiteSpaceType>;
142144
export const textOverflowProperty: CssProperty<Style, CoreTypes.TextOverflowType>;
143145
export const letterSpacingProperty: CssProperty<Style, number>;

0 commit comments

Comments
 (0)








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/NativeScript/NativeScript/commit/d6478237ec4f4d8fa4b457f39bddcc463e90643c

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy